From 6b6c2fa5a0579a2873dc61435f3bb833be43c9e3 Mon Sep 17 00:00:00 2001
From: Florent PIGOUT
Date: Thu, 11 Feb 2016 08:58:19 +0100
Subject: [PATCH 001/255] Get assertion when response signature reference URI
is empty
---
src/onelogin/saml2/response.py | 3 +++
...esponse_with_unsigned_assertion.xml.base64 | 1 +
.../src/OneLogin/saml2_tests/response_test.py | 20 +++++++++++++++++++
3 files changed, 24 insertions(+)
create mode 100644 tests/data/responses/valid_response_with_unsigned_assertion.xml.base64
diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py
index c4c51a5b..5fb13574 100644
--- a/src/onelogin/saml2/response.py
+++ b/src/onelogin/saml2/response.py
@@ -417,8 +417,11 @@ def __query_assertion(self, xpath_expr):
# Check if the message is signed
signed_message_query = '/samlp:Response' + signature_expr
message_reference_nodes = self.__query(signed_message_query)
+ # we can have reference node but URI can be empty
+ message_id = None
if message_reference_nodes:
message_id = message_reference_nodes[0].get('URI')
+ if message_id:
final_query = "/samlp:Response[@ID='%s']/" % message_id[1:]
else:
final_query = "/samlp:Response"
diff --git a/tests/data/responses/valid_response_with_unsigned_assertion.xml.base64 b/tests/data/responses/valid_response_with_unsigned_assertion.xml.base64
new file mode 100644
index 00000000..06b1919b
--- /dev/null
+++ b/tests/data/responses/valid_response_with_unsigned_assertion.xml.base64
@@ -0,0 +1 @@
+PD94bWwgdmVyc2lvbj0iMS4wIj8+CjxzYW1scDpSZXNwb25zZSB4bWxuczpzYW1scD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiB4bWxuczpzYW1sPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIiBJRD0icGZ4MDVmM2NlMTAtMTYxNS1mM2VhLWE5ODgtNjBlMzgwYjMyOTlmIiBWZXJzaW9uPSIyLjAiIElzc3VlSW5zdGFudD0iMjAxNC0wMi0xOVQwMTozNzowMVoiIERlc3RpbmF0aW9uPSJodHRwczovL3BpdGJ1bGsubm8taXAub3JnL25ld29uZWxvZ2luL2RlbW8xL2luZGV4LnBocD9hY3MiIEluUmVzcG9uc2VUbz0iT05FTE9HSU5fNWZlOWQ2ZTQ5OWIyZjA5MTMyMDZhYWIzZjcxOTE3MjkwNDliYjgwNyI+CiAgPHNhbWw6SXNzdWVyPmh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvc2ltcGxlc2FtbC9zYW1sMi9pZHAvbWV0YWRhdGEucGhwPC9zYW1sOklzc3Vlcj4KICA8ZHM6U2lnbmF0dXJlIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj4KICAgIDxkczpTaWduZWRJbmZvPgogICAgICA8ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPgogICAgICA8ZHM6U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3JzYS1zaGExIi8+CiAgICAgIDxkczpSZWZlcmVuY2UgVVJJPSIiPgogICAgICAgIDxkczpUcmFuc2Zvcm1zPgogICAgICAgICAgPGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+CiAgICAgICAgICA8ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+CiAgICAgICAgPC9kczpUcmFuc2Zvcm1zPgogICAgICAgIDxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjc2hhMSIvPgogICAgICAgIDxkczpEaWdlc3RWYWx1ZT51VFlSOWFBcHBLdWp3YkY3azRUOFczLzBaZDA9PC9kczpEaWdlc3RWYWx1ZT4KICAgICAgPC9kczpSZWZlcmVuY2U+CiAgICA8L2RzOlNpZ25lZEluZm8+CiAgICA8ZHM6U2lnbmF0dXJlVmFsdWU+MnYxSDBSaVNJUEtYdEdUcTh3L3hpZHRrci9SWXZUVU90MXdCRmZlaXRhT0wyQkt1Z0JobEFoQyt2U2FVeFoweQpOUHpIdW1odlMzRHZkV2xmUjNzZlNra2VJeDZtYTd3blB2eko3MHZna09QRnF1UFRRQmdJdTVzMnMwUXJoZGgrCmYrU1dEWkpEbTNQeGpPbkhEQ3FrRE1Sc1VhejhtYzV3M0tWQnozQlhRaEU9PC9kczpTaWduYXR1cmVWYWx1ZT4KICA8L2RzOlNpZ25hdHVyZT4KICA8c2FtbHA6U3RhdHVzPgogICAgPHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPgogIDwvc2FtbHA6U3RhdHVzPgogIDxzYW1sOkFzc2VydGlvbiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIElEPSJwZnhiNGVjOWM4YS00OGViLWZkYTItN2Y3NC1mYTFhMTA1YTk5ZmUiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDE0LTAyLTE5VDAxOjM3OjAxWiI+CiAgICA8c2FtbDpJc3N1ZXI+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPgogICAgPHNhbWw6U3ViamVjdD4KICAgICAgPHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9tZXRhZGF0YS5waHAiIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6MS4xOm5hbWVpZC1mb3JtYXQ6ZW1haWxBZGRyZXNzIj40OTI4ODI2MTVhY2YzMWM4MDk2YjYyNzI0NWQ3NmFlNTMwMzZjMDkwPC9zYW1sOk5hbWVJRD4KICAgICAgPHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPgogICAgICAgIDxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAyMy0wOC0yM1QwNjo1NzowMVoiIFJlY2lwaWVudD0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciLz4KICAgICAgPC9zYW1sOlN1YmplY3RDb25maXJtYXRpb24+CiAgICA8L3NhbWw6U3ViamVjdD4KICAgIDxzYW1sOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDE0LTAyLTE5VDAxOjM2OjMxWiIgTm90T25PckFmdGVyPSIyMDIzLTA4LTIzVDA2OjU3OjAxWiI+CiAgICAgIDxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+CiAgICAgICAgPHNhbWw6QXVkaWVuY2U+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9tZXRhZGF0YS5waHA8L3NhbWw6QXVkaWVuY2U+CiAgICAgIDwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPgogICAgPC9zYW1sOkNvbmRpdGlvbnM+CiAgICA8c2FtbDpBdXRoblN0YXRlbWVudCBBdXRobkluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIiBTZXNzaW9uTm90T25PckFmdGVyPSIyMDE0LTAyLTE5VDA5OjM3OjAxWiIgU2Vzc2lvbkluZGV4PSJfNjI3M2Q3N2I4Y2RlMGMzMzNlYzc5ZDIyYTlmYTAwMDNiOWZlMmQ3NWNiIj4KICAgICAgPHNhbWw6QXV0aG5Db250ZXh0PgogICAgICAgIDxzYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkPC9zYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPgogICAgICA8L3NhbWw6QXV0aG5Db250ZXh0PgogICAgPC9zYW1sOkF1dGhuU3RhdGVtZW50PgogICAgPHNhbWw6QXR0cmlidXRlU3RhdGVtZW50PgogICAgICA8c2FtbDpBdHRyaWJ1dGUgTmFtZT0idWlkIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj4KICAgICAgICA8c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5zbWFydGluPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPgogICAgICA8L3NhbWw6QXR0cmlidXRlPgogICAgICA8c2FtbDpBdHRyaWJ1dGUgTmFtZT0ibWFpbCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+CiAgICAgICAgPHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+c21hcnRpbkB5YWNvLmVzPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPgogICAgICA8L3NhbWw6QXR0cmlidXRlPgogICAgICA8c2FtbDpBdHRyaWJ1dGUgTmFtZT0iY24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPgogICAgICAgIDxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPlNpeHRvMzwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT4KICAgICAgPC9zYW1sOkF0dHJpYnV0ZT4KICAgICAgPHNhbWw6QXR0cmlidXRlIE5hbWU9InNuIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj4KICAgICAgICA8c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5NYXJ0aW4yPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPgogICAgICA8L3NhbWw6QXR0cmlidXRlPgogICAgICA8c2FtbDpBdHRyaWJ1dGUgTmFtZT0iZWR1UGVyc29uQWZmaWxpYXRpb24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPgogICAgICAgIDxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnVzZXI8L3NhbWw6QXR0cmlidXRlVmFsdWU+CiAgICAgICAgPHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+YWRtaW48L3NhbWw6QXR0cmlidXRlVmFsdWU+CiAgICAgIDwvc2FtbDpBdHRyaWJ1dGU+CiAgICA8L3NhbWw6QXR0cmlidXRlU3RhdGVtZW50PgogIDwvc2FtbDpBc3NlcnRpb24+Cjwvc2FtbHA6UmVzcG9uc2U+Cg==
\ No newline at end of file
diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py
index daf1c2d2..f7fafcca 100644
--- a/tests/src/OneLogin/saml2_tests/response_test.py
+++ b/tests/src/OneLogin/saml2_tests/response_test.py
@@ -207,6 +207,26 @@ def testQueryAssertions(self):
response_7 = OneLogin_Saml2_Response(settings, xml_7)
self.assertEqual(['http://idp.example.com/'], response_7.get_issuers())
+ def testQueryAssertionsWithEmptyRefenceURI(self):
+ """
+ Tests the __query_assertion if //Signature/Reference/@URI is empty.
+ """
+ settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
+
+ # test with signed assertion still work
+ xml = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64'))
+ response = OneLogin_Saml2_Response(settings, xml)
+ self.assertEqual('492882615acf31c8096b627245d76ae53036c090', response.get_nameid())
+
+ # test with unsigned assertion still work
+ xml = self.file_contents(join(self.data_path, 'responses', 'valid_response_with_unsigned_assertion.xml.base64'))
+ response = OneLogin_Saml2_Response(settings, xml)
+ self.assertEqual('492882615acf31c8096b627245d76ae53036c090', response.get_nameid())
+
+ xml = self.file_contents(join(self.data_path, 'responses', 'response_without_reference_uri.xml.base64'))
+ response = OneLogin_Saml2_Response(settings, xml)
+ self.assertEqual('saml@user.com', response.get_nameid())
+
def testGetIssuers(self):
"""
Tests the get_issuers method of the OneLogin_Saml2_Response
From eafae456df6d5931428801ee6524fac5a1b44f7d Mon Sep 17 00:00:00 2001
From: Florent
Date: Fri, 12 Feb 2016 12:10:37 +0100
Subject: [PATCH 002/255] Modify signature reference URI only when use inner
signature certificate
When Response should be verifiy with the IdP certificate and the signature
reference URI is empty we can not modify the Response content or it will be
considered as invalid, ex.:
$ xmlsec1 --sign \
--id-attr:ID urn:oasis:names:tc:SAML:2.0:protocol:Response \
--privkey-pem python-saml/metadata.key \
--output python-saml/signed_assertion.xml \
python-saml/unsigned_assertion.xml
$ xmlsec1 --verify \
--id-attr:ID urn:oasis:names:tc:SAML:2.0:protocol:Response \
--pubkey-cert python-saml/metadata.crt \
python-saml/signed_assertion.xml
OK
$ python -c "
from onelogin.saml2.utils import OneLogin_Saml2_Utils
with open('python-saml/signed_assertion.xml') as assertion:
with open('python-saml/metadata.crt') as cert:
print OneLogin_Saml2_Utils.validate_sign(assertion.read(), cert.read(), debug=True)
"
signatures.c:346(xmlSecOpenSSLEvpSignatureVerify) obj=rsa-sha1 subject=EVP_VerifyFinal msg=signature do not match errno=18
False
On another hand we should continue to do that if we validate the xml
with an internal certificate.
---
src/onelogin/saml2/utils.py | 12 ++++++------
tests/src/OneLogin/saml2_tests/response_test.py | 6 ++++++
2 files changed, 12 insertions(+), 6 deletions(-)
diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py
index d7ade21d..844eaf3b 100644
--- a/src/onelogin/saml2/utils.py
+++ b/src/onelogin/saml2/utils.py
@@ -1051,15 +1051,15 @@ def validate_node_sign(signature_node, elem, cert=None, fingerprint=None, finger
if fingerprint == x509_fingerprint_value:
cert = OneLogin_Saml2_Utils.format_cert(x509_cert_value)
+ # Check if Reference URI is empty
+ reference_elem = OneLogin_Saml2_Utils.query(signature_node, '//ds:Reference')
+ if len(reference_elem) > 0:
+ if reference_elem[0].get('URI') == '':
+ reference_elem[0].set('URI', '#%s' % signature_node.getparent().get('ID'))
+
if cert is None or cert == '':
return False
- # Check if Reference URI is empty
- reference_elem = OneLogin_Saml2_Utils.query(signature_node, '//ds:Reference')
- if len(reference_elem) > 0:
- if reference_elem[0].get('URI') == '':
- reference_elem[0].set('URI', '#%s' % signature_node.getparent().get('ID'))
-
dsig_ctx = xmlsec.DSigCtx()
file_cert = OneLogin_Saml2_Utils.write_temp_file(cert)
diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py
index f7fafcca..53b00662 100644
--- a/tests/src/OneLogin/saml2_tests/response_test.py
+++ b/tests/src/OneLogin/saml2_tests/response_test.py
@@ -1218,6 +1218,12 @@ def testIsValidSignWithEmptyReferenceURI(self):
response = OneLogin_Saml2_Response(settings, xml)
self.assertTrue(response.is_valid(self.get_request_data()))
+ def testIsValidSignWithEmptyReferenceURIAndIdPCert(self):
+ settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
+ xml = self.file_contents(join(self.data_path, 'responses', 'valid_response_with_unsigned_assertion.xml.base64'))
+ response = OneLogin_Saml2_Response(settings, xml)
+ self.assertTrue(response.is_valid(self.get_request_data()))
+
def testIsValidWithoutInResponseTo(self):
"""
If assertion contains InResponseTo but not the Response tag, we should
From 5b49b9127cda43676dcf7f5d4014dc582d6d3f44 Mon Sep 17 00:00:00 2001
From: Florent
Date: Tue, 16 Feb 2016 14:58:21 +0100
Subject: [PATCH 003/255] Remove useless statement
---
src/onelogin/saml2/utils.py | 2 --
1 file changed, 2 deletions(-)
diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py
index 844eaf3b..1fafe770 100644
--- a/src/onelogin/saml2/utils.py
+++ b/src/onelogin/saml2/utils.py
@@ -1060,8 +1060,6 @@ def validate_node_sign(signature_node, elem, cert=None, fingerprint=None, finger
if cert is None or cert == '':
return False
- dsig_ctx = xmlsec.DSigCtx()
-
file_cert = OneLogin_Saml2_Utils.write_temp_file(cert)
if validatecert:
From c12d308e3b81afea22ed201e56d5423aaa2a7285 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Fri, 15 Apr 2016 16:27:50 +0200
Subject: [PATCH 004/255] Revert "Get/validate assertion when empty Response
signature ref URI"
---
src/onelogin/saml2/response.py | 3 ---
src/onelogin/saml2/utils.py | 14 +++++-----
...esponse_with_unsigned_assertion.xml.base64 | 1 -
.../src/OneLogin/saml2_tests/response_test.py | 26 -------------------
4 files changed, 8 insertions(+), 36 deletions(-)
delete mode 100644 tests/data/responses/valid_response_with_unsigned_assertion.xml.base64
diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py
index 5fb13574..c4c51a5b 100644
--- a/src/onelogin/saml2/response.py
+++ b/src/onelogin/saml2/response.py
@@ -417,11 +417,8 @@ def __query_assertion(self, xpath_expr):
# Check if the message is signed
signed_message_query = '/samlp:Response' + signature_expr
message_reference_nodes = self.__query(signed_message_query)
- # we can have reference node but URI can be empty
- message_id = None
if message_reference_nodes:
message_id = message_reference_nodes[0].get('URI')
- if message_id:
final_query = "/samlp:Response[@ID='%s']/" % message_id[1:]
else:
final_query = "/samlp:Response"
diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py
index f6fc76dd..d3595cb3 100644
--- a/src/onelogin/saml2/utils.py
+++ b/src/onelogin/saml2/utils.py
@@ -1051,15 +1051,17 @@ def validate_node_sign(signature_node, elem, cert=None, fingerprint=None, finger
if fingerprint == x509_fingerprint_value:
cert = OneLogin_Saml2_Utils.format_cert(x509_cert_value)
- # Check if Reference URI is empty
- reference_elem = OneLogin_Saml2_Utils.query(signature_node, '//ds:Reference')
- if len(reference_elem) > 0:
- if reference_elem[0].get('URI') == '':
- reference_elem[0].set('URI', '#%s' % signature_node.getparent().get('ID'))
-
if cert is None or cert == '':
return False
+ # Check if Reference URI is empty
+ reference_elem = OneLogin_Saml2_Utils.query(signature_node, '//ds:Reference')
+ if len(reference_elem) > 0:
+ if reference_elem[0].get('URI') == '':
+ reference_elem[0].set('URI', '#%s' % signature_node.getparent().get('ID'))
+
+ dsig_ctx = xmlsec.DSigCtx()
+
file_cert = OneLogin_Saml2_Utils.write_temp_file(cert)
if validatecert:
diff --git a/tests/data/responses/valid_response_with_unsigned_assertion.xml.base64 b/tests/data/responses/valid_response_with_unsigned_assertion.xml.base64
deleted file mode 100644
index 06b1919b..00000000
--- a/tests/data/responses/valid_response_with_unsigned_assertion.xml.base64
+++ /dev/null
@@ -1 +0,0 @@
-PD94bWwgdmVyc2lvbj0iMS4wIj8+CjxzYW1scDpSZXNwb25zZSB4bWxuczpzYW1scD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiB4bWxuczpzYW1sPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIiBJRD0icGZ4MDVmM2NlMTAtMTYxNS1mM2VhLWE5ODgtNjBlMzgwYjMyOTlmIiBWZXJzaW9uPSIyLjAiIElzc3VlSW5zdGFudD0iMjAxNC0wMi0xOVQwMTozNzowMVoiIERlc3RpbmF0aW9uPSJodHRwczovL3BpdGJ1bGsubm8taXAub3JnL25ld29uZWxvZ2luL2RlbW8xL2luZGV4LnBocD9hY3MiIEluUmVzcG9uc2VUbz0iT05FTE9HSU5fNWZlOWQ2ZTQ5OWIyZjA5MTMyMDZhYWIzZjcxOTE3MjkwNDliYjgwNyI+CiAgPHNhbWw6SXNzdWVyPmh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvc2ltcGxlc2FtbC9zYW1sMi9pZHAvbWV0YWRhdGEucGhwPC9zYW1sOklzc3Vlcj4KICA8ZHM6U2lnbmF0dXJlIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj4KICAgIDxkczpTaWduZWRJbmZvPgogICAgICA8ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPgogICAgICA8ZHM6U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3JzYS1zaGExIi8+CiAgICAgIDxkczpSZWZlcmVuY2UgVVJJPSIiPgogICAgICAgIDxkczpUcmFuc2Zvcm1zPgogICAgICAgICAgPGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+CiAgICAgICAgICA8ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+CiAgICAgICAgPC9kczpUcmFuc2Zvcm1zPgogICAgICAgIDxkczpEaWdlc3RNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjc2hhMSIvPgogICAgICAgIDxkczpEaWdlc3RWYWx1ZT51VFlSOWFBcHBLdWp3YkY3azRUOFczLzBaZDA9PC9kczpEaWdlc3RWYWx1ZT4KICAgICAgPC9kczpSZWZlcmVuY2U+CiAgICA8L2RzOlNpZ25lZEluZm8+CiAgICA8ZHM6U2lnbmF0dXJlVmFsdWU+MnYxSDBSaVNJUEtYdEdUcTh3L3hpZHRrci9SWXZUVU90MXdCRmZlaXRhT0wyQkt1Z0JobEFoQyt2U2FVeFoweQpOUHpIdW1odlMzRHZkV2xmUjNzZlNra2VJeDZtYTd3blB2eko3MHZna09QRnF1UFRRQmdJdTVzMnMwUXJoZGgrCmYrU1dEWkpEbTNQeGpPbkhEQ3FrRE1Sc1VhejhtYzV3M0tWQnozQlhRaEU9PC9kczpTaWduYXR1cmVWYWx1ZT4KICA8L2RzOlNpZ25hdHVyZT4KICA8c2FtbHA6U3RhdHVzPgogICAgPHNhbWxwOlN0YXR1c0NvZGUgVmFsdWU9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpzdGF0dXM6U3VjY2VzcyIvPgogIDwvc2FtbHA6U3RhdHVzPgogIDxzYW1sOkFzc2VydGlvbiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIElEPSJwZnhiNGVjOWM4YS00OGViLWZkYTItN2Y3NC1mYTFhMTA1YTk5ZmUiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDE0LTAyLTE5VDAxOjM3OjAxWiI+CiAgICA8c2FtbDpJc3N1ZXI+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPgogICAgPHNhbWw6U3ViamVjdD4KICAgICAgPHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9tZXRhZGF0YS5waHAiIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6MS4xOm5hbWVpZC1mb3JtYXQ6ZW1haWxBZGRyZXNzIj40OTI4ODI2MTVhY2YzMWM4MDk2YjYyNzI0NWQ3NmFlNTMwMzZjMDkwPC9zYW1sOk5hbWVJRD4KICAgICAgPHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPgogICAgICAgIDxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAyMy0wOC0yM1QwNjo1NzowMVoiIFJlY2lwaWVudD0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciLz4KICAgICAgPC9zYW1sOlN1YmplY3RDb25maXJtYXRpb24+CiAgICA8L3NhbWw6U3ViamVjdD4KICAgIDxzYW1sOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDE0LTAyLTE5VDAxOjM2OjMxWiIgTm90T25PckFmdGVyPSIyMDIzLTA4LTIzVDA2OjU3OjAxWiI+CiAgICAgIDxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+CiAgICAgICAgPHNhbWw6QXVkaWVuY2U+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9tZXRhZGF0YS5waHA8L3NhbWw6QXVkaWVuY2U+CiAgICAgIDwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPgogICAgPC9zYW1sOkNvbmRpdGlvbnM+CiAgICA8c2FtbDpBdXRoblN0YXRlbWVudCBBdXRobkluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIiBTZXNzaW9uTm90T25PckFmdGVyPSIyMDE0LTAyLTE5VDA5OjM3OjAxWiIgU2Vzc2lvbkluZGV4PSJfNjI3M2Q3N2I4Y2RlMGMzMzNlYzc5ZDIyYTlmYTAwMDNiOWZlMmQ3NWNiIj4KICAgICAgPHNhbWw6QXV0aG5Db250ZXh0PgogICAgICAgIDxzYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkPC9zYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPgogICAgICA8L3NhbWw6QXV0aG5Db250ZXh0PgogICAgPC9zYW1sOkF1dGhuU3RhdGVtZW50PgogICAgPHNhbWw6QXR0cmlidXRlU3RhdGVtZW50PgogICAgICA8c2FtbDpBdHRyaWJ1dGUgTmFtZT0idWlkIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj4KICAgICAgICA8c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5zbWFydGluPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPgogICAgICA8L3NhbWw6QXR0cmlidXRlPgogICAgICA8c2FtbDpBdHRyaWJ1dGUgTmFtZT0ibWFpbCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+CiAgICAgICAgPHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+c21hcnRpbkB5YWNvLmVzPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPgogICAgICA8L3NhbWw6QXR0cmlidXRlPgogICAgICA8c2FtbDpBdHRyaWJ1dGUgTmFtZT0iY24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPgogICAgICAgIDxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPlNpeHRvMzwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT4KICAgICAgPC9zYW1sOkF0dHJpYnV0ZT4KICAgICAgPHNhbWw6QXR0cmlidXRlIE5hbWU9InNuIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj4KICAgICAgICA8c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5NYXJ0aW4yPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPgogICAgICA8L3NhbWw6QXR0cmlidXRlPgogICAgICA8c2FtbDpBdHRyaWJ1dGUgTmFtZT0iZWR1UGVyc29uQWZmaWxpYXRpb24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPgogICAgICAgIDxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnVzZXI8L3NhbWw6QXR0cmlidXRlVmFsdWU+CiAgICAgICAgPHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+YWRtaW48L3NhbWw6QXR0cmlidXRlVmFsdWU+CiAgICAgIDwvc2FtbDpBdHRyaWJ1dGU+CiAgICA8L3NhbWw6QXR0cmlidXRlU3RhdGVtZW50PgogIDwvc2FtbDpBc3NlcnRpb24+Cjwvc2FtbHA6UmVzcG9uc2U+Cg==
\ No newline at end of file
diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py
index 53b00662..daf1c2d2 100644
--- a/tests/src/OneLogin/saml2_tests/response_test.py
+++ b/tests/src/OneLogin/saml2_tests/response_test.py
@@ -207,26 +207,6 @@ def testQueryAssertions(self):
response_7 = OneLogin_Saml2_Response(settings, xml_7)
self.assertEqual(['http://idp.example.com/'], response_7.get_issuers())
- def testQueryAssertionsWithEmptyRefenceURI(self):
- """
- Tests the __query_assertion if //Signature/Reference/@URI is empty.
- """
- settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
-
- # test with signed assertion still work
- xml = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64'))
- response = OneLogin_Saml2_Response(settings, xml)
- self.assertEqual('492882615acf31c8096b627245d76ae53036c090', response.get_nameid())
-
- # test with unsigned assertion still work
- xml = self.file_contents(join(self.data_path, 'responses', 'valid_response_with_unsigned_assertion.xml.base64'))
- response = OneLogin_Saml2_Response(settings, xml)
- self.assertEqual('492882615acf31c8096b627245d76ae53036c090', response.get_nameid())
-
- xml = self.file_contents(join(self.data_path, 'responses', 'response_without_reference_uri.xml.base64'))
- response = OneLogin_Saml2_Response(settings, xml)
- self.assertEqual('saml@user.com', response.get_nameid())
-
def testGetIssuers(self):
"""
Tests the get_issuers method of the OneLogin_Saml2_Response
@@ -1218,12 +1198,6 @@ def testIsValidSignWithEmptyReferenceURI(self):
response = OneLogin_Saml2_Response(settings, xml)
self.assertTrue(response.is_valid(self.get_request_data()))
- def testIsValidSignWithEmptyReferenceURIAndIdPCert(self):
- settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
- xml = self.file_contents(join(self.data_path, 'responses', 'valid_response_with_unsigned_assertion.xml.base64'))
- response = OneLogin_Saml2_Response(settings, xml)
- self.assertTrue(response.is_valid(self.get_request_data()))
-
def testIsValidWithoutInResponseTo(self):
"""
If assertion contains InResponseTo but not the Response tag, we should
From e6d8bb7b5816746117bbf97420ef1e7d1f08135a Mon Sep 17 00:00:00 2001
From: chriskohlbrenner
Date: Mon, 18 Apr 2016 15:36:39 -0400
Subject: [PATCH 005/255] [docs] minor spelling improvements to typos in readme
---
README.md | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/README.md b/README.md
index 5c8aa09e..3118b929 100644
--- a/README.md
+++ b/README.md
@@ -135,7 +135,7 @@ SAML requires a x.509 cert to sign and encrypt elements like NameID, Message, As
If our environment requires sign or encrypt support, the certs folder may contain the x509 cert and the private key that the SP will use:
* sp.crt The public cert of the SP
-* sp.key The privake key of the SP
+* sp.key The private key of the SP
Or also we can provide those data in the setting file at the 'x509cert' and the privateKey' json parameters of the 'sp' element.
@@ -225,7 +225,7 @@ This is the settings.json file:
},
// If you need to specify requested attributes, set a
// attributeConsumingService. nameFormat, attributeValue and
- // friendlyName can be ommited
+ // friendlyName can be omitted
"attributeConsumingService": {
"ServiceName": "SP test",
"serviceDescription": "Test Service",
@@ -356,7 +356,7 @@ In addition to the required settings data (idp, sp), there is extra information
// Authentication context.
// Set to false and no AuthContext will be sent in the AuthNRequest,
- // Set true or don't present thi parameter and you will get an AuthContext 'exact' 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport'
+ // Set true or don't present this parameter and you will get an AuthContext 'exact' 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport'
// Set an array with the possible auth context values: array ('urn:oasis:names:tc:SAML:2.0:ac:classes:Password', 'urn:oasis:names:tc:SAML:2.0:ac:classes:X509'),
'requestedAuthnContext': true,
// Allows the authn comparison parameter to be set, defaults to 'exact' if the setting is not present.
@@ -378,7 +378,7 @@ In addition to the required settings data (idp, sp), there is extra information
'signatureAlgorithm' => 'http://www.w3.org/2000/09/xmldsig#rsa-sha1'
},
- // Contact information template, it is recommended to suply a
+ // Contact information template, it is recommended to supply
// technical and support contacts.
"contactPerson": {
"technical": {
@@ -392,7 +392,7 @@ In addition to the required settings data (idp, sp), there is extra information
},
// Organization information template, the info in en_US lang is
- // recomended, add more if required.
+ // recommended, add more if required.
"organization": {
"en-US": {
"name": "sp_test",
From d61f78d59861087719559cb8f0b6e0646e934f51 Mon Sep 17 00:00:00 2001
From: Chad Bibler
Date: Fri, 22 Apr 2016 15:44:26 -0500
Subject: [PATCH 006/255] Fix typo in test
---
tests/src/OneLogin/saml2_tests/utils_test.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tests/src/OneLogin/saml2_tests/utils_test.py b/tests/src/OneLogin/saml2_tests/utils_test.py
index d84adf65..7bc49f95 100644
--- a/tests/src/OneLogin/saml2_tests/utils_test.py
+++ b/tests/src/OneLogin/saml2_tests/utils_test.py
@@ -734,7 +734,7 @@ def testDecryptElement(self):
except:
pass
- key_3_file_name = join(self.data_path, 'misc', 'sp2.key')
+ key_3_file_name = join(self.data_path, 'misc', 'sp3.key')
f = open(key_3_file_name, 'r')
key3 = f.read()
f.close()
From b4037992f97b6ee21afc37e752f5a3888ef284d9 Mon Sep 17 00:00:00 2001
From: Chad Bibler
Date: Fri, 22 Apr 2016 15:45:27 -0500
Subject: [PATCH 007/255] Assert failure for the expected reason
---
tests/src/OneLogin/saml2_tests/utils_test.py | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/tests/src/OneLogin/saml2_tests/utils_test.py b/tests/src/OneLogin/saml2_tests/utils_test.py
index 7bc49f95..f93f7da9 100644
--- a/tests/src/OneLogin/saml2_tests/utils_test.py
+++ b/tests/src/OneLogin/saml2_tests/utils_test.py
@@ -731,8 +731,8 @@ def testDecryptElement(self):
try:
OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key2)
self.assertTrue(False)
- except:
- pass
+ except Exception as e:
+ self.assertEqual('failed to decrypt', e[0])
key_3_file_name = join(self.data_path, 'misc', 'sp3.key')
f = open(key_3_file_name, 'r')
@@ -741,8 +741,8 @@ def testDecryptElement(self):
try:
OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key3)
self.assertTrue(False)
- except:
- pass
+ except Exception as e:
+ self.assertEqual('failed to decrypt', e[0])
xml_nameid_enc_2 = b64decode(self.file_contents(join(self.data_path, 'responses', 'invalids', 'encrypted_nameID_without_EncMethod.xml.base64')))
dom_nameid_enc_2 = parseString(xml_nameid_enc_2)
From 12f3d3946f4800ddca0d2824832f75fbbe0b5d03 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Thu, 28 Apr 2016 17:19:07 +0200
Subject: [PATCH 008/255] IdP metadata parser: fix singleSignOnService
processing
---
src/onelogin/saml2/idp_metadata_parser.py | 4 ++--
src/onelogin/saml2/utils.py | 14 +++++++-------
.../saml2_tests/idp_metadata_parser_test.py | 6 +++---
tests/src/OneLogin/saml2_tests/response_test.py | 2 +-
4 files changed, 13 insertions(+), 13 deletions(-)
diff --git a/src/onelogin/saml2/idp_metadata_parser.py b/src/onelogin/saml2/idp_metadata_parser.py
index 0634c177..6a396d60 100644
--- a/src/onelogin/saml2/idp_metadata_parser.py
+++ b/src/onelogin/saml2/idp_metadata_parser.py
@@ -117,8 +117,8 @@ def parse(idp_metadata):
if idp_entity_id is not None:
data['idp']['entityId'] = idp_entity_id
if idp_sso_url is not None:
- data['idp']['singleLogoutService'] = {}
- data['idp']['singleLogoutService']['url'] = idp_sso_url
+ data['idp']['singleSignOnService'] = {}
+ data['idp']['singleSignOnService']['url'] = idp_sso_url
if idp_slo_url is not None:
data['idp']['singleLogoutService'] = {}
data['idp']['singleLogoutService']['url'] = idp_slo_url
diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py
index d3595cb3..06a6f5cd 100644
--- a/src/onelogin/saml2/utils.py
+++ b/src/onelogin/saml2/utils.py
@@ -1051,16 +1051,16 @@ def validate_node_sign(signature_node, elem, cert=None, fingerprint=None, finger
if fingerprint == x509_fingerprint_value:
cert = OneLogin_Saml2_Utils.format_cert(x509_cert_value)
- if cert is None or cert == '':
- return False
# Check if Reference URI is empty
- reference_elem = OneLogin_Saml2_Utils.query(signature_node, '//ds:Reference')
- if len(reference_elem) > 0:
- if reference_elem[0].get('URI') == '':
- reference_elem[0].set('URI', '#%s' % signature_node.getparent().get('ID'))
+ # reference_elem = OneLogin_Saml2_Utils.query(signature_node, '//ds:Reference')
+ # if len(reference_elem) > 0:
+ # if reference_elem[0].get('URI') == '':
+ # reference_elem[0].set('URI', '#%s' % signature_node.getparent().get('ID'))
- dsig_ctx = xmlsec.DSigCtx()
+
+ if cert is None or cert == '':
+ return False
file_cert = OneLogin_Saml2_Utils.write_temp_file(cert)
diff --git a/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py b/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py
index 85158b49..8e1c5c14 100644
--- a/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py
+++ b/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py
@@ -53,7 +53,7 @@ def testParseRemote(self):
data = OneLogin_Saml2_IdPMetadataParser.parse_remote('https://www.testshib.org/metadata/testshib-providers.xml')
self.assertTrue(data is not None and data is not {})
- expected_data = {'sp': {'NameIDFormat': 'urn:mace:shibboleth:1.0:nameIdentifier'}, 'idp': {'singleLogoutService': {'url': 'https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO'}, 'entityId': 'https://idp.testshib.org/idp/shibboleth'}}
+ expected_data = {'sp': {'NameIDFormat': 'urn:mace:shibboleth:1.0:nameIdentifier'}, 'idp': {'singleSignOnService': {'url': 'https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO'}, 'entityId': 'https://idp.testshib.org/idp/shibboleth'}}
self.assertEqual(expected_data, data)
def testParse(self):
@@ -69,7 +69,7 @@ def testParse(self):
xml_idp_metadata = self.file_contents(join(self.data_path, 'metadata', 'idp_metadata.xml'))
data = OneLogin_Saml2_IdPMetadataParser.parse(xml_idp_metadata)
- expected_data = {'sp': {'NameIDFormat': 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'}, 'idp': {'singleLogoutService': {'url': 'https://app.onelogin.com/trust/saml2/http-post/sso/383123'}, 'entityId': 'https://app.onelogin.com/saml/metadata/383123', 'x509cert': 'MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzET\nMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREwDwYD\nVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbTAeFw0xMzA2\nMDUxNzE2MjBaFw0xODA2MDUxNzE2MjBaMGcxCzAJBgNVBAYTAlVTMRMwEQYDVQQI\nDApDYWxpZm9ybmlhMRUwEwYDVQQHDAxTYW50YSBNb25pY2ExETAPBgNVBAoMCE9u\nZUxvZ2luMRkwFwYDVQQDDBBhcHAub25lbG9naW4uY29tMIIBIjANBgkqhkiG9w0B\nAQEFAAOCAQ8AMIIBCgKCAQEAse8rnep4qL2GmhH10pMQyJ2Jae+AQHyfgVjaQZ7Z\n0QQog5jX91vcJRSMi0XWJnUtOr6lF0dq1+yckjZ92wyLrH+7fvngNO1aV4Mjk9sT\ngf+iqMrae6y6fRxDt9PXrEFVjvd3vv7QTJf2FuIPy4vVP06Dt8EMkQIr8rmLmU0m\nTr1k2DkrdtdlCuNFTXuAu3QqfvNCRrRwfNObn9MP6JeOUdcGLJsBjGF8exfcN1SF\nzRF0JFr3dmOlx761zK5liD0T1sYWnDquatj/JD9fZMbKecBKni1NglH/LVd+b6aJ\nUAr5LulERULUjLqYJRKW31u91/4Qazdo9tbvwqyFxaoUrwIDAQABo4HUMIHRMAwG\nA1UdEwEB/wQCMAAwHQYDVR0OBBYEFPWcXvQSlTXnzZD2xziuoUvrrDedMIGRBgNV\nHSMEgYkwgYaAFPWcXvQSlTXnzZD2xziuoUvrrDedoWukaTBnMQswCQYDVQQGEwJV\nUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREw\nDwYDVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbYIBATAO\nBgNVHQ8BAf8EBAMCBPAwDQYJKoZIhvcNAQEFBQADggEBAB/8xe3rzqXQVxzHyAHu\nAuPa73ClDoL1cko0Fp8CGcqEIyj6Te9gx5z6wyfv+Lo8RFvBLlnB1lXqbC+fTGcV\ngG/4oKLJ5UwRFxInqpZPnOAudVNnd0PYOODn9FWs6u+OTIQIaIcPUv3MhB9lwHIJ\nsTk/bs9xcru5TPyLIxLLd6ib/pRceKH2mTkzUd0DYk9CQNXXeoGx/du5B9nh3ClP\nTbVakRzl3oswgI5MQIphYxkW70SopEh4kOFSRE1ND31NNIq1YrXlgtkguQBFsZWu\nQOPR6cEwFZzP0tHTYbI839WgxX6hfhIUTUz6mLqq4+3P4BG3+1OXeVDg63y8Uh78\n1sE='}}
+ expected_data = {'sp': {'NameIDFormat': 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'}, 'idp': {'singleSignOnService': {'url': 'https://app.onelogin.com/trust/saml2/http-post/sso/383123'}, 'entityId': 'https://app.onelogin.com/saml/metadata/383123', 'x509cert': 'MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzET\nMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREwDwYD\nVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbTAeFw0xMzA2\nMDUxNzE2MjBaFw0xODA2MDUxNzE2MjBaMGcxCzAJBgNVBAYTAlVTMRMwEQYDVQQI\nDApDYWxpZm9ybmlhMRUwEwYDVQQHDAxTYW50YSBNb25pY2ExETAPBgNVBAoMCE9u\nZUxvZ2luMRkwFwYDVQQDDBBhcHAub25lbG9naW4uY29tMIIBIjANBgkqhkiG9w0B\nAQEFAAOCAQ8AMIIBCgKCAQEAse8rnep4qL2GmhH10pMQyJ2Jae+AQHyfgVjaQZ7Z\n0QQog5jX91vcJRSMi0XWJnUtOr6lF0dq1+yckjZ92wyLrH+7fvngNO1aV4Mjk9sT\ngf+iqMrae6y6fRxDt9PXrEFVjvd3vv7QTJf2FuIPy4vVP06Dt8EMkQIr8rmLmU0m\nTr1k2DkrdtdlCuNFTXuAu3QqfvNCRrRwfNObn9MP6JeOUdcGLJsBjGF8exfcN1SF\nzRF0JFr3dmOlx761zK5liD0T1sYWnDquatj/JD9fZMbKecBKni1NglH/LVd+b6aJ\nUAr5LulERULUjLqYJRKW31u91/4Qazdo9tbvwqyFxaoUrwIDAQABo4HUMIHRMAwG\nA1UdEwEB/wQCMAAwHQYDVR0OBBYEFPWcXvQSlTXnzZD2xziuoUvrrDedMIGRBgNV\nHSMEgYkwgYaAFPWcXvQSlTXnzZD2xziuoUvrrDedoWukaTBnMQswCQYDVQQGEwJV\nUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREw\nDwYDVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbYIBATAO\nBgNVHQ8BAf8EBAMCBPAwDQYJKoZIhvcNAQEFBQADggEBAB/8xe3rzqXQVxzHyAHu\nAuPa73ClDoL1cko0Fp8CGcqEIyj6Te9gx5z6wyfv+Lo8RFvBLlnB1lXqbC+fTGcV\ngG/4oKLJ5UwRFxInqpZPnOAudVNnd0PYOODn9FWs6u+OTIQIaIcPUv3MhB9lwHIJ\nsTk/bs9xcru5TPyLIxLLd6ib/pRceKH2mTkzUd0DYk9CQNXXeoGx/du5B9nh3ClP\nTbVakRzl3oswgI5MQIphYxkW70SopEh4kOFSRE1ND31NNIq1YrXlgtkguQBFsZWu\nQOPR6cEwFZzP0tHTYbI839WgxX6hfhIUTUz6mLqq4+3P4BG3+1OXeVDg63y8Uh78\n1sE='}}
self.assertEqual(expected_data, data)
def testMergeSettings(self):
@@ -86,7 +86,7 @@ def testMergeSettings(self):
data = OneLogin_Saml2_IdPMetadataParser.parse(xml_idp_metadata)
settings = self.loadSettingsJSON()
settings_result = OneLogin_Saml2_IdPMetadataParser.merge_settings(settings, data)
- expected_data = {u'sp': {'NameIDFormat': 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'}, u'idp': {'singleLogoutService': {'url': 'https://app.onelogin.com/trust/saml2/http-post/sso/383123'}, 'entityId': 'https://app.onelogin.com/saml/metadata/383123', 'x509cert': 'MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzET\nMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREwDwYD\nVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbTAeFw0xMzA2\nMDUxNzE2MjBaFw0xODA2MDUxNzE2MjBaMGcxCzAJBgNVBAYTAlVTMRMwEQYDVQQI\nDApDYWxpZm9ybmlhMRUwEwYDVQQHDAxTYW50YSBNb25pY2ExETAPBgNVBAoMCE9u\nZUxvZ2luMRkwFwYDVQQDDBBhcHAub25lbG9naW4uY29tMIIBIjANBgkqhkiG9w0B\nAQEFAAOCAQ8AMIIBCgKCAQEAse8rnep4qL2GmhH10pMQyJ2Jae+AQHyfgVjaQZ7Z\n0QQog5jX91vcJRSMi0XWJnUtOr6lF0dq1+yckjZ92wyLrH+7fvngNO1aV4Mjk9sT\ngf+iqMrae6y6fRxDt9PXrEFVjvd3vv7QTJf2FuIPy4vVP06Dt8EMkQIr8rmLmU0m\nTr1k2DkrdtdlCuNFTXuAu3QqfvNCRrRwfNObn9MP6JeOUdcGLJsBjGF8exfcN1SF\nzRF0JFr3dmOlx761zK5liD0T1sYWnDquatj/JD9fZMbKecBKni1NglH/LVd+b6aJ\nUAr5LulERULUjLqYJRKW31u91/4Qazdo9tbvwqyFxaoUrwIDAQABo4HUMIHRMAwG\nA1UdEwEB/wQCMAAwHQYDVR0OBBYEFPWcXvQSlTXnzZD2xziuoUvrrDedMIGRBgNV\nHSMEgYkwgYaAFPWcXvQSlTXnzZD2xziuoUvrrDedoWukaTBnMQswCQYDVQQGEwJV\nUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREw\nDwYDVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbYIBATAO\nBgNVHQ8BAf8EBAMCBPAwDQYJKoZIhvcNAQEFBQADggEBAB/8xe3rzqXQVxzHyAHu\nAuPa73ClDoL1cko0Fp8CGcqEIyj6Te9gx5z6wyfv+Lo8RFvBLlnB1lXqbC+fTGcV\ngG/4oKLJ5UwRFxInqpZPnOAudVNnd0PYOODn9FWs6u+OTIQIaIcPUv3MhB9lwHIJ\nsTk/bs9xcru5TPyLIxLLd6ib/pRceKH2mTkzUd0DYk9CQNXXeoGx/du5B9nh3ClP\nTbVakRzl3oswgI5MQIphYxkW70SopEh4kOFSRE1ND31NNIq1YrXlgtkguQBFsZWu\nQOPR6cEwFZzP0tHTYbI839WgxX6hfhIUTUz6mLqq4+3P4BG3+1OXeVDg63y8Uh78\n1sE='}, u'strict': False, u'contactPerson': {u'technical': {u'givenName': u'technical_name', u'emailAddress': u'technical@example.com'}, u'support': {u'givenName': u'support_name', u'emailAddress': u'support@example.com'}}, u'debug': False, u'organization': {u'en-US': {u'url': u'http://sp.example.com', u'displayname': u'SP test', u'name': u'sp_test'}}, u'security': {u'signMetadata': False, u'wantAssertionsSigned': False, u'authnRequestsSigned': False}, u'custom_base_path': u'../../../tests/data/customPath/'}
+ expected_data = {u'sp': {'NameIDFormat': 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'}, u'idp': {'singleSignOnService': {'url': 'https://app.onelogin.com/trust/saml2/http-post/sso/383123'}, 'entityId': 'https://app.onelogin.com/saml/metadata/383123', 'x509cert': 'MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzET\nMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREwDwYD\nVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbTAeFw0xMzA2\nMDUxNzE2MjBaFw0xODA2MDUxNzE2MjBaMGcxCzAJBgNVBAYTAlVTMRMwEQYDVQQI\nDApDYWxpZm9ybmlhMRUwEwYDVQQHDAxTYW50YSBNb25pY2ExETAPBgNVBAoMCE9u\nZUxvZ2luMRkwFwYDVQQDDBBhcHAub25lbG9naW4uY29tMIIBIjANBgkqhkiG9w0B\nAQEFAAOCAQ8AMIIBCgKCAQEAse8rnep4qL2GmhH10pMQyJ2Jae+AQHyfgVjaQZ7Z\n0QQog5jX91vcJRSMi0XWJnUtOr6lF0dq1+yckjZ92wyLrH+7fvngNO1aV4Mjk9sT\ngf+iqMrae6y6fRxDt9PXrEFVjvd3vv7QTJf2FuIPy4vVP06Dt8EMkQIr8rmLmU0m\nTr1k2DkrdtdlCuNFTXuAu3QqfvNCRrRwfNObn9MP6JeOUdcGLJsBjGF8exfcN1SF\nzRF0JFr3dmOlx761zK5liD0T1sYWnDquatj/JD9fZMbKecBKni1NglH/LVd+b6aJ\nUAr5LulERULUjLqYJRKW31u91/4Qazdo9tbvwqyFxaoUrwIDAQABo4HUMIHRMAwG\nA1UdEwEB/wQCMAAwHQYDVR0OBBYEFPWcXvQSlTXnzZD2xziuoUvrrDedMIGRBgNV\nHSMEgYkwgYaAFPWcXvQSlTXnzZD2xziuoUvrrDedoWukaTBnMQswCQYDVQQGEwJV\nUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREw\nDwYDVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbYIBATAO\nBgNVHQ8BAf8EBAMCBPAwDQYJKoZIhvcNAQEFBQADggEBAB/8xe3rzqXQVxzHyAHu\nAuPa73ClDoL1cko0Fp8CGcqEIyj6Te9gx5z6wyfv+Lo8RFvBLlnB1lXqbC+fTGcV\ngG/4oKLJ5UwRFxInqpZPnOAudVNnd0PYOODn9FWs6u+OTIQIaIcPUv3MhB9lwHIJ\nsTk/bs9xcru5TPyLIxLLd6ib/pRceKH2mTkzUd0DYk9CQNXXeoGx/du5B9nh3ClP\nTbVakRzl3oswgI5MQIphYxkW70SopEh4kOFSRE1ND31NNIq1YrXlgtkguQBFsZWu\nQOPR6cEwFZzP0tHTYbI839WgxX6hfhIUTUz6mLqq4+3P4BG3+1OXeVDg63y8Uh78\n1sE='}, u'strict': False, u'contactPerson': {u'technical': {u'givenName': u'technical_name', u'emailAddress': u'technical@example.com'}, u'support': {u'givenName': u'support_name', u'emailAddress': u'support@example.com'}}, u'debug': False, u'organization': {u'en-US': {u'url': u'http://sp.example.com', u'displayname': u'SP test', u'name': u'sp_test'}}, u'security': {u'signMetadata': False, u'wantAssertionsSigned': False, u'authnRequestsSigned': False}, u'custom_base_path': u'../../../tests/data/customPath/'}
self.assertEqual(expected_data, settings_result)
expected_data2 = {'sp': {u'singleLogoutService': {u'url': u'http://stuff.com/endpoints/endpoints/sls.php'}, u'assertionConsumerService': {u'url': u'http://stuff.com/endpoints/endpoints/acs.php'}, u'entityId': u'http://stuff.com/endpoints/metadata.php', u'NameIDFormat': u'urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified'}, 'idp': {u'singleLogoutService': {u'url': u'http://idp.example.com/SingleLogoutService.php'}, u'entityId': u'http://idp.example.com/', u'x509cert': u'MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo', u'singleSignOnService': {u'url': u'http://idp.example.com/SSOService.php'}}, u'strict': False, u'contactPerson': {u'technical': {u'givenName': u'technical_name', u'emailAddress': u'technical@example.com'}, u'support': {u'givenName': u'support_name', u'emailAddress': u'support@example.com'}}, u'debug': False, u'organization': {u'en-US': {u'url': u'http://sp.example.com', u'displayname': u'SP test', u'name': u'sp_test'}}, u'security': {u'signMetadata': False, u'wantAssertionsSigned': False, u'authnRequestsSigned': False}, u'custom_base_path': u'../../../tests/data/customPath/'}
diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py
index daf1c2d2..7041e04f 100644
--- a/tests/src/OneLogin/saml2_tests/response_test.py
+++ b/tests/src/OneLogin/saml2_tests/response_test.py
@@ -1196,7 +1196,7 @@ def testIsValidSignWithEmptyReferenceURI(self):
settings = OneLogin_Saml2_Settings(settings_info)
xml = self.file_contents(join(self.data_path, 'responses', 'response_without_reference_uri.xml.base64'))
response = OneLogin_Saml2_Response(settings, xml)
- self.assertTrue(response.is_valid(self.get_request_data()))
+ self.assertFalse(response.is_valid(self.get_request_data()))
def testIsValidWithoutInResponseTo(self):
"""
From 414e05e3f3de6f03b3f66d398eda2e37f4342eb0 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Tue, 10 May 2016 17:02:11 +0200
Subject: [PATCH 009/255] Fix #119. Allow AuthnRequest with no NameIDPolicy
---
README.md | 7 ++--
src/onelogin/saml2/auth.py | 12 ++++--
src/onelogin/saml2/authn_request.py | 26 ++++++++-----
src/onelogin/saml2/utils.py | 2 -
tests/src/OneLogin/saml2_tests/auth_test.py | 38 ++++++++++++++++++-
.../saml2_tests/authn_request_test.py | 28 ++++++++++++++
6 files changed, 93 insertions(+), 20 deletions(-)
diff --git a/README.md b/README.md
index 3118b929..6a93e657 100644
--- a/README.md
+++ b/README.md
@@ -518,10 +518,11 @@ We can set a 'return_to' url parameter to the login function and that will be co
target_url = 'https://example.com'
auth.login(return_to=target_url)
```
-The login method can recieve 2 more optional parameters:
+The login method can recieve 3 more optional parameters:
-* force_authn When true the AuthNReuqest will set the ForceAuthn='true'
-* is_passive When true the AuthNReuqest will set the Ispassive='true'
+* force_authn When true the AuthNReuqest will set the ForceAuthn='true'
+* is_passive When true the AuthNReuqest will set the Ispassive='true'
+* set_nameid_policy When true the AuthNReuqest will set a nameIdPolicy element.
If a match on the future SAMLResponse ID and the AuthNRequest ID to be sent is required, that AuthNRequest ID must to be extracted and stored for future validation, we can get that ID by
diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py
index a41a3f92..26e13e28 100644
--- a/src/onelogin/saml2/auth.py
+++ b/src/onelogin/saml2/auth.py
@@ -267,7 +267,7 @@ def get_last_request_id(self):
"""
return self.__last_request_id
- def login(self, return_to=None, force_authn=False, is_passive=False):
+ def login(self, return_to=None, force_authn=False, is_passive=False, set_nameid_policy=True):
"""
Initiates the SSO process.
@@ -275,14 +275,18 @@ def login(self, return_to=None, force_authn=False, is_passive=False):
:type return_to: string
:param force_authn: Optional argument. When true the AuthNReuqest will set the ForceAuthn='true'.
- :type force_authn: string
+ :type force_authn: bool
:param is_passive: Optional argument. When true the AuthNReuqest will set the Ispassive='true'.
- :type is_passive: string
+ :type is_passive: bool
+
+ :param set_nameid_policy: Optional argument. When true the AuthNReuqest will set a nameIdPolicy element.
+ :type set_nameid_policy: bool
:returns: Redirection url
+ :rtype: string
"""
- authn_request = OneLogin_Saml2_Authn_Request(self.__settings, force_authn, is_passive)
+ authn_request = OneLogin_Saml2_Authn_Request(self.__settings, force_authn, is_passive, set_nameid_policy)
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 81d53200..452a1975 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):
+ def __init__(self, settings, force_authn=False, is_passive=False, set_nameid_policy=True):
"""
Constructs the AuthnRequest object.
@@ -34,6 +34,9 @@ def __init__(self, settings, force_authn=False, is_passive=False):
:param is_passive: Optional argument. When true the AuthNReuqest will set the Ispassive='true'.
:type is_passive: bool
+
+ :param set_nameid_policy: Optional argument. When true the AuthNReuqest will set a nameIdPolicy element.
+ :type set_nameid_policy: bool
"""
self.__settings = settings
@@ -47,10 +50,6 @@ def __init__(self, settings, force_authn=False, is_passive=False):
destination = idp_data['singleSignOnService']['url']
- name_id_policy_format = sp_data['NameIDFormat']
- if 'wantNameIdEncrypted' in security and security['wantNameIdEncrypted']:
- name_id_policy_format = OneLogin_Saml2_Constants.NAMEID_ENCRYPTED
-
provider_name_str = ''
organization_data = settings.get_organization()
if isinstance(organization_data, dict) and organization_data:
@@ -70,6 +69,17 @@ def __init__(self, settings, force_authn=False, is_passive=False):
if is_passive is True:
is_passive_str = 'IsPassive="true"'
+ nameid_policy_str = ''
+ if set_nameid_policy:
+ name_id_policy_format = sp_data['NameIDFormat']
+ if 'wantNameIdEncrypted' in security and security['wantNameIdEncrypted']:
+ name_id_policy_format = OneLogin_Saml2_Constants.NAMEID_ENCRYPTED
+
+ nameid_policy_str = """
+ """ % name_id_policy_format
+
requested_authn_context_str = ''
if 'requestedAuthnContext' in security.keys() and security['requestedAuthnContext'] is not False:
authn_comparison = 'exact'
@@ -104,9 +114,7 @@ def __init__(self, settings, force_authn=False, is_passive=False):
AssertionConsumerServiceURL="%(assertion_url)s"
%(attr_consuming_service_str)s>
%(entity_id)s
-
+%(nameid_policy_str)s
%(requested_authn_context_str)s
""" % \
{
@@ -118,7 +126,7 @@ def __init__(self, settings, force_authn=False, is_passive=False):
'destination': destination,
'assertion_url': sp_data['assertionConsumerService']['url'],
'entity_id': sp_data['entityId'],
- 'name_id_policy': name_id_policy_format,
+ '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/utils.py b/src/onelogin/saml2/utils.py
index 06a6f5cd..eccbc5bd 100644
--- a/src/onelogin/saml2/utils.py
+++ b/src/onelogin/saml2/utils.py
@@ -1051,14 +1051,12 @@ def validate_node_sign(signature_node, elem, cert=None, fingerprint=None, finger
if fingerprint == x509_fingerprint_value:
cert = OneLogin_Saml2_Utils.format_cert(x509_cert_value)
-
# Check if Reference URI is empty
# reference_elem = OneLogin_Saml2_Utils.query(signature_node, '//ds:Reference')
# if len(reference_elem) > 0:
# if reference_elem[0].get('URI') == '':
# reference_elem[0].set('URI', '#%s' % signature_node.getparent().get('ID'))
-
if cert is None or cert == '':
return False
diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py
index e75dfef7..1bc0a577 100644
--- a/tests/src/OneLogin/saml2_tests/auth_test.py
+++ b/tests/src/OneLogin/saml2_tests/auth_test.py
@@ -608,7 +608,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 Login with no parameters. A 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 Login with no parameters. A AuthN Request is built with IsPassive and redirect executed
"""
settings_info = self.loadSettingsJSON()
return_to = u'http://example.com/returnto'
@@ -673,6 +673,40 @@ def testLoginIsPassive(self):
request_3 = OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query_3['SAMLRequest'][0])
self.assertIn('IsPassive="true"', request_3)
+ 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
+ """
+ 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])
+ sso_url = settings_info['idp']['singleSignOnService']['url']
+ self.assertIn(sso_url, target_url)
+ self.assertIn('SAMLRequest', parsed_query)
+ request = OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query['SAMLRequest'][0])
+ self.assertIn('
Date: Tue, 10 May 2016 17:31:51 +0200
Subject: [PATCH 010/255] Improve AuthNRequest format
---
src/onelogin/saml2/authn_request.py | 19 +++++++------------
1 file changed, 7 insertions(+), 12 deletions(-)
diff --git a/src/onelogin/saml2/authn_request.py b/src/onelogin/saml2/authn_request.py
index 452a1975..ea0200e8 100644
--- a/src/onelogin/saml2/authn_request.py
+++ b/src/onelogin/saml2/authn_request.py
@@ -59,15 +59,15 @@ def __init__(self, settings, force_authn=False, is_passive=False, set_nameid_pol
else:
lang = langs[0]
if 'displayname' in organization_data[lang] and organization_data[lang]['displayname'] is not None:
- provider_name_str = 'ProviderName="%s"' % organization_data[lang]['displayname']
+ provider_name_str = "\n" + ' ProviderName="%s"' % organization_data[lang]['displayname']
force_authn_str = ''
if force_authn is True:
- force_authn_str = 'ForceAuthn="true"'
+ force_authn_str = "\n" + ' ForceAuthn="true"'
is_passive_str = ''
if is_passive is True:
- is_passive_str = 'IsPassive="true"'
+ is_passive_str = "\n" + ' IsPassive="true"'
nameid_policy_str = ''
if set_nameid_policy:
@@ -87,11 +87,11 @@ def __init__(self, settings, force_authn=False, is_passive=False, set_nameid_pol
authn_comparison = security['requestedAuthnContextComparison']
if security['requestedAuthnContext'] is True:
- requested_authn_context_str = """
+ requested_authn_context_str = "\n" + """
urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport
""" % authn_comparison
else:
- requested_authn_context_str = ' ' % authn_comparison
+ requested_authn_context_str = "\n" + ' ' % authn_comparison
for authn_context in security['requestedAuthnContext']:
requested_authn_context_str += '%s ' % authn_context
requested_authn_context_str += ' '
@@ -104,18 +104,13 @@ def __init__(self, settings, force_authn=False, is_passive=False, set_nameid_pol
xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
ID="%(id)s"
- Version="2.0"
- %(provider_name)s
- %(force_authn_str)s
- %(is_passive_str)s
+ Version="2.0"%(provider_name)s%(force_authn_str)s%(is_passive_str)s
IssueInstant="%(issue_instant)s"
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
-%(requested_authn_context_str)s
+ %(entity_id)s %(nameid_policy_str)s%(requested_authn_context_str)s
""" % \
{
'id': uid,
From 4c14943005f7c787ac59a2b9ad395d3fc565ebd1 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Wed, 11 May 2016 21:34:27 +0200
Subject: [PATCH 011/255] #112. Remove requirement on responsesRemove
---
README.md | 4 ++
demo-bottle/saml/advanced_settings.json | 1 +
demo-django/saml/advanced_settings.json | 1 +
demo-flask/saml/advanced_settings.json | 1 +
src/onelogin/saml2/auth.py | 4 +-
src/onelogin/saml2/response.py | 24 +++++---
src/onelogin/saml2/settings.py | 4 ++
.../src/OneLogin/saml2_tests/response_test.py | 60 ++++++++++++++++++-
.../src/OneLogin/saml2_tests/settings_test.py | 41 +++++++++++++
9 files changed, 129 insertions(+), 11 deletions(-)
diff --git a/README.md b/README.md
index 6a93e657..07b96dc8 100644
--- a/README.md
+++ b/README.md
@@ -347,6 +347,10 @@ In addition to the required settings data (idp, sp), there is extra information
// this SP to be signed. [Metadata of the SP will offer this info]
"wantAssertionsSigned": false,
+ // Indicates a requirement for the NameID element on the SAMLResponse
+ // received by this SP to be present.
+ "wantNameId": true,
+
// Indicates a requirement for the NameID received by
// this SP to be encrypted.
"wantNameIdEncrypted": false,
diff --git a/demo-bottle/saml/advanced_settings.json b/demo-bottle/saml/advanced_settings.json
index 4ea002ad..78e5662c 100644
--- a/demo-bottle/saml/advanced_settings.json
+++ b/demo-bottle/saml/advanced_settings.json
@@ -7,6 +7,7 @@
"signMetadata": false,
"wantMessagesSigned": false,
"wantAssertionsSigned": false,
+ "wantNameId" : true,
"wantNameIdEncrypted": false,
"signatureAlgorithm": "http://www.w3.org/2000/09/xmldsig#rsa-sha1"
},
diff --git a/demo-django/saml/advanced_settings.json b/demo-django/saml/advanced_settings.json
index 97f3a374..04bd0fba 100644
--- a/demo-django/saml/advanced_settings.json
+++ b/demo-django/saml/advanced_settings.json
@@ -7,6 +7,7 @@
"signMetadata": false,
"wantMessagesSigned": false,
"wantAssertionsSigned": false,
+ "wantNameId" : true,
"wantNameIdEncrypted": false,
"signatureAlgorithm": "http://www.w3.org/2000/09/xmldsig#rsa-sha1"
},
diff --git a/demo-flask/saml/advanced_settings.json b/demo-flask/saml/advanced_settings.json
index 97f3a374..04bd0fba 100644
--- a/demo-flask/saml/advanced_settings.json
+++ b/demo-flask/saml/advanced_settings.json
@@ -7,6 +7,7 @@
"signMetadata": false,
"wantMessagesSigned": false,
"wantAssertionsSigned": false,
+ "wantNameId" : true,
"wantNameIdEncrypted": false,
"signatureAlgorithm": "http://www.w3.org/2000/09/xmldsig#rsa-sha1"
},
diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py
index 26e13e28..ff06da50 100644
--- a/src/onelogin/saml2/auth.py
+++ b/src/onelogin/saml2/auth.py
@@ -206,7 +206,7 @@ def get_nameid(self):
Returns the nameID.
:returns: NameID
- :rtype: string
+ :rtype: string|None
"""
return self.__nameid
@@ -222,7 +222,7 @@ def get_session_expiration(self):
"""
Returns the SessionNotOnOrAfter from the AuthnStatement.
:returns: The SessionNotOnOrAfter of the assertion
- :rtype: DateTime|null
+ :rtype: DateTime|None
"""
return self.__session_expiration
diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py
index c4c51a5b..77453b86 100644
--- a/src/onelogin/saml2/response.py
+++ b/src/onelogin/saml2/response.py
@@ -276,6 +276,8 @@ def get_nameid_data(self):
:rtype: dict
"""
nameid = None
+ nameid_data = {}
+
encrypted_id_data_nodes = self.__query_assertion('/saml:Subject/saml:EncryptedID/xenc:EncryptedData')
if encrypted_id_data_nodes:
encrypted_data = encrypted_id_data_nodes[0]
@@ -286,13 +288,16 @@ def get_nameid_data(self):
if nameid_nodes:
nameid = nameid_nodes[0]
if nameid is None:
- raise Exception('Not NameID found in the assertion of the Response')
+ security = self.__settings.get_security_data()
- nameid_data = {'Value': nameid.text}
- for attr in ['Format', 'SPNameQualifier', 'NameQualifier']:
- value = nameid.get(attr, None)
- if value:
- nameid_data[attr] = value
+ if security.get('wantNameId', True):
+ raise Exception('Not NameID found in the assertion of the Response')
+ else:
+ nameid_data = {'Value': nameid.text}
+ for attr in ['Format', 'SPNameQualifier', 'NameQualifier']:
+ value = nameid.get(attr, None)
+ if value:
+ nameid_data[attr] = value
return nameid_data
def get_nameid(self):
@@ -300,10 +305,13 @@ def get_nameid(self):
Gets the NameID provided by the SAML Response from the IdP
:returns: NameID (value)
- :rtype: string
+ :rtype: string|None
"""
+ nameid_value = None
nameid_data = self.get_nameid_data()
- return nameid_data['Value']
+ if nameid_data and 'Value' in nameid_data.keys():
+ nameid_value = nameid_data['Value']
+ return nameid_value
def get_session_not_on_or_after(self):
"""
diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py
index 12b6e1df..60d8cdb5 100644
--- a/src/onelogin/saml2/settings.py
+++ b/src/onelogin/saml2/settings.py
@@ -293,6 +293,10 @@ def __add_default_values(self):
if 'wantAssertionsSigned' not in self.__security.keys():
self.__security['wantAssertionsSigned'] = False
+ # NameID element expected
+ if 'wantNameId' not in self.__security.keys():
+ self.__security['wantNameId'] = True
+
# Encrypt expected
if 'wantAssertionsEncrypted' not in self.__security.keys():
self.__security['wantAssertionsEncrypted'] = False
diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py
index 7041e04f..34bea8d6 100644
--- a/tests/src/OneLogin/saml2_tests/response_test.py
+++ b/tests/src/OneLogin/saml2_tests/response_test.py
@@ -64,7 +64,9 @@ def testReturnNameId(self):
"""
Tests the get_nameid method of the OneLogin_Saml2_Response
"""
- settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
+ json_settings = self.loadSettingsJSON()
+
+ settings = OneLogin_Saml2_Settings(json_settings)
xml = self.file_contents(join(self.data_path, 'responses', 'response1.xml.base64'))
response = OneLogin_Saml2_Response(settings, xml)
self.assertEqual('support@onelogin.com', response.get_nameid())
@@ -85,10 +87,39 @@ def testReturnNameId(self):
except Exception as e:
self.assertIn('Not NameID found in the assertion of the Response', e.message)
+ json_settings['security']['wantNameId'] = True
+ settings = OneLogin_Saml2_Settings(json_settings)
+
+ response_5 = OneLogin_Saml2_Response(settings, xml_4)
+ try:
+ response_5.get_nameid()
+ self.assertTrue(False)
+ except Exception as e:
+ self.assertIn('Not NameID found in the assertion of the Response', e.message)
+
+ json_settings['security']['wantNameId'] = False
+ settings = OneLogin_Saml2_Settings(json_settings)
+
+ response_6 = OneLogin_Saml2_Response(settings, xml_4)
+ nameid_6 = response_6.get_nameid()
+ self.assertIsNone(nameid_6)
+
+ del json_settings['security']['wantNameId']
+ settings = OneLogin_Saml2_Settings(json_settings)
+
+ response_7 = OneLogin_Saml2_Response(settings, xml_4)
+ try:
+ response_7.get_nameid()
+ self.assertTrue(False)
+ except Exception as e:
+ self.assertIn('Not NameID found in the assertion of the Response', e.message)
+
def testGetNameIdData(self):
"""
Tests the get_nameid_data method of the OneLogin_Saml2_Response
"""
+ json_settings = self.loadSettingsJSON()
+
settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
xml = self.file_contents(join(self.data_path, 'responses', 'response1.xml.base64'))
response = OneLogin_Saml2_Response(settings, xml)
@@ -127,6 +158,33 @@ def testGetNameIdData(self):
except Exception as e:
self.assertIn('Not NameID found in the assertion of the Response', e.message)
+ json_settings['security']['wantNameId'] = True
+ settings = OneLogin_Saml2_Settings(json_settings)
+
+ response_5 = OneLogin_Saml2_Response(settings, xml_4)
+ try:
+ response_5.get_nameid_data()
+ self.assertTrue(False)
+ except Exception as e:
+ self.assertIn('Not NameID found in the assertion of the Response', e.message)
+
+ json_settings['security']['wantNameId'] = False
+ settings = OneLogin_Saml2_Settings(json_settings)
+
+ response_6 = OneLogin_Saml2_Response(settings, xml_4)
+ nameid_data_6 = response_6.get_nameid_data()
+ self.assertEqual({}, nameid_data_6)
+
+ del json_settings['security']['wantNameId']
+ settings = OneLogin_Saml2_Settings(json_settings)
+
+ response_7 = OneLogin_Saml2_Response(settings, xml_4)
+ try:
+ response_7.get_nameid_data()
+ self.assertTrue(False)
+ except Exception as e:
+ self.assertIn('Not NameID found in the assertion of the Response', e.message)
+
def testCheckStatus(self):
"""
Tests the check_status method of the OneLogin_Saml2_Response
diff --git a/tests/src/OneLogin/saml2_tests/settings_test.py b/tests/src/OneLogin/saml2_tests/settings_test.py
index 6fdd4703..398e09a1 100644
--- a/tests/src/OneLogin/saml2_tests/settings_test.py
+++ b/tests/src/OneLogin/saml2_tests/settings_test.py
@@ -621,8 +621,49 @@ def testGetSecurityData(self):
self.assertIn('signMetadata', security)
self.assertIn('wantMessagesSigned', security)
self.assertIn('wantAssertionsSigned', security)
+ self.assertIn('requestedAuthnContext', security)
+ self.assertIn('wantNameId', security)
self.assertIn('wantNameIdEncrypted', security)
+ def testGetDefaultSecurityValues(self):
+ """
+ Tests default values of Security advanced sesettings
+ """
+ settings_json = self.loadSettingsJSON()
+ del settings_json['security']
+ settings = OneLogin_Saml2_Settings(settings_json)
+ security = settings.get_security_data()
+
+ self.assertIn('nameIdEncrypted', security)
+ self.assertFalse(security.get('nameIdEncrypted'))
+
+ self.assertIn('authnRequestsSigned', security)
+ self.assertFalse(security.get('authnRequestsSigned'))
+
+ self.assertIn('logoutRequestSigned', security)
+ self.assertFalse(security.get('logoutRequestSigned'))
+
+ self.assertIn('logoutResponseSigned', security)
+ self.assertFalse(security.get('logoutResponseSigned'))
+
+ self.assertIn('signMetadata', security)
+ self.assertFalse(security.get('signMetadata'))
+
+ self.assertIn('wantMessagesSigned', security)
+ self.assertFalse(security.get('wantMessagesSigned'))
+
+ self.assertIn('wantAssertionsSigned', security)
+ self.assertFalse(security.get('wantAssertionsSigned'))
+
+ self.assertIn('requestedAuthnContext', security)
+ self.assertTrue(security.get('requestedAuthnContext'))
+
+ self.assertIn('wantNameId', security)
+ self.assertTrue(security.get('wantNameId'))
+
+ self.assertIn('wantNameIdEncrypted', security)
+ self.assertFalse(security.get('wantNameIdEncrypted'))
+
def testGetContacts(self):
"""
Tests the getContacts method of the OneLogin_Saml2_Settings
From 10dc84f974ec10a98ea418a80884d78878568959 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Sat, 14 May 2016 12:43:17 +0200
Subject: [PATCH 012/255] IdP metadata parser/merger adjustments
---
src/onelogin/saml2/idp_metadata_parser.py | 85 ++++-
.../saml2_tests/idp_metadata_parser_test.py | 337 +++++++++++++++++-
2 files changed, 399 insertions(+), 23 deletions(-)
diff --git a/src/onelogin/saml2/idp_metadata_parser.py b/src/onelogin/saml2/idp_metadata_parser.py
index 6a396d60..44c3d4d5 100644
--- a/src/onelogin/saml2/idp_metadata_parser.py
+++ b/src/onelogin/saml2/idp_metadata_parser.py
@@ -10,6 +10,8 @@
"""
import urllib2
+
+from copy import deepcopy
from defusedxml.lxml import fromstring
from onelogin.saml2.constants import OneLogin_Saml2_Constants
@@ -51,7 +53,7 @@ def get_metadata(url):
return xml
@staticmethod
- def parse_remote(url):
+ def parse_remote(url, **kwargs):
"""
Get the metadata XML from the provided URL and parse it, returning a dict with extracted data
@@ -62,22 +64,41 @@ def parse_remote(url):
:rtype: dict
"""
idp_metadata = OneLogin_Saml2_IdPMetadataParser.get_metadata(url)
- return OneLogin_Saml2_IdPMetadataParser.parse(idp_metadata)
+ return OneLogin_Saml2_IdPMetadataParser.parse(idp_metadata, **kwargs)
@staticmethod
- def parse(idp_metadata):
+ def parse(
+ idp_metadata,
+ required_sso_binding=OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT,
+ required_slo_binding=OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT):
"""
- Parse the Identity Provider metadata and returns a dict with extracted data
- If there are multiple IDPSSODescriptor it will only parse the first
+ Parse the Identity Provider metadata and return a dict with extracted data.
+
+ If there are multiple tags, parse only the first.
+
+ Parse only those SSO endpoints with the same binding as given by
+ the `required_sso_binding` parameter.
+
+ Parse only those SLO endpoints with the same binding as given by
+ the `required_slo_binding` parameter.
+
+ If the metadata specifies multiple SSO endpoints with the required
+ binding, extract only the first (the same holds true for SLO
+ endpoints).
:param idp_metadata: XML of the Identity Provider Metadata.
:type idp_metadata: string
- :param url: If true and the URL is HTTPs, the cert of the domain is checked.
- :type url: bool
+ :param required_sso_binding: Parse only POST or REDIRECT SSO endpoints.
+ :type required_sso_binding: one of OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT
+ or OneLogin_Saml2_Constants.BINDING_HTTP_POST
+
+ :param required_slo_binding: Parse only POST or REDIRECT SLO endpoints.
+ :type required_slo_binding: one of OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT
+ or OneLogin_Saml2_Constants.BINDING_HTTP_POST
:returns: settings dict with extracted data
- :rtype: string
+ :rtype: dict
"""
data = {}
@@ -100,11 +121,18 @@ def parse(idp_metadata):
if len(name_id_format_nodes) > 0:
idp_name_id_format = name_id_format_nodes[0].text
- sso_nodes = OneLogin_Saml2_Utils.query(idp_descriptor_node, "./md:SingleSignOnService[@Binding='%s']" % OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT)
+ sso_nodes = OneLogin_Saml2_Utils.query(
+ idp_descriptor_node,
+ "./md:SingleSignOnService[@Binding='%s']" % required_sso_binding
+ )
+
if len(sso_nodes) > 0:
idp_sso_url = sso_nodes[0].get('Location', None)
- slo_nodes = OneLogin_Saml2_Utils.query(idp_descriptor_node, "./md:SingleLogoutService[@Binding='%s']" % OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT)
+ slo_nodes = OneLogin_Saml2_Utils.query(
+ idp_descriptor_node,
+ "./md:SingleLogoutService[@Binding='%s']" % required_slo_binding
+ )
if len(slo_nodes) > 0:
idp_slo_url = slo_nodes[0].get('Location', None)
@@ -116,12 +144,17 @@ def parse(idp_metadata):
if idp_entity_id is not None:
data['idp']['entityId'] = idp_entity_id
+
if idp_sso_url is not None:
data['idp']['singleSignOnService'] = {}
data['idp']['singleSignOnService']['url'] = idp_sso_url
+ data['idp']['singleSignOnService']['binding'] = required_sso_binding
+
if idp_slo_url is not None:
data['idp']['singleLogoutService'] = {}
data['idp']['singleLogoutService']['url'] = idp_slo_url
+ data['idp']['singleLogoutService']['binding'] = required_slo_binding
+
if idp_x509_cert is not None:
data['idp']['x509cert'] = idp_x509_cert
@@ -150,6 +183,34 @@ def merge_settings(settings, new_metadata_settings):
:returns: merged settings
:rtype: dict
"""
- result_settings = settings.copy()
- result_settings.update(new_metadata_settings)
+ for d in (settings, new_metadata_settings):
+ if not isinstance(d, dict):
+ raise TypeError('Both arguments must be dictionaries.')
+
+ # Guarantee to not modify original data (`settings.copy()` would not
+ # be sufficient, as it's just a shallow copy).
+ result_settings = deepcopy(settings)
+ # Merge `new_metadata_settings` into `result_settings`.
+ dict_deep_merge(result_settings, new_metadata_settings)
return result_settings
+
+
+def dict_deep_merge(a, b, path=None):
+ """Deep-merge dictionary `b` into dictionary `a`.
+ Kudos to http://stackoverflow.com/a/7205107/145400
+ """
+ if path is None:
+ path = []
+ for key in b:
+ if key in a:
+ if isinstance(a[key], dict) and isinstance(b[key], dict):
+ dict_deep_merge(a[key], b[key], path + [str(key)])
+ elif a[key] == b[key]:
+ # Key conflict, but equal value.
+ pass
+ else:
+ # Key/value conflict. Prioritize b over a.
+ a[key] = b[key]
+ else:
+ a[key] = b[key]
+ return a
diff --git a/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py b/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py
index 8e1c5c14..18beb65b 100644
--- a/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py
+++ b/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py
@@ -4,17 +4,25 @@
# All rights reserved.
+from copy import deepcopy
import json
from os.path import dirname, join, exists
from lxml.etree import XMLSyntaxError
import unittest
+from urllib2 import URLError
from teamcity import is_running_under_teamcity
from teamcity.unittestpy import TeamcityTestRunner
from onelogin.saml2.idp_metadata_parser import OneLogin_Saml2_IdPMetadataParser
+from onelogin.saml2.constants import OneLogin_Saml2_Constants
class OneLogin_Saml2_IdPMetadataParser_Test(unittest.TestCase):
+ # Instruct unittest to not hide diffs upon test failure, even for complex
+ # dictionaries. This prevents the message "Diff is 907 characters long.
+ # Set self.maxDiff to None to see it." from showing up.
+ maxDiff = None
+
data_path = join(dirname(dirname(dirname(dirname(__file__)))), 'data')
settings_path = join(dirname(dirname(dirname(dirname(__file__)))), 'settings')
@@ -41,7 +49,10 @@ def testGetMetadata(self):
with self.assertRaises(Exception):
data = OneLogin_Saml2_IdPMetadataParser.get_metadata('http://google.es')
- data = OneLogin_Saml2_IdPMetadataParser.get_metadata('https://www.testshib.org/metadata/testshib-providers.xml')
+ try:
+ data = OneLogin_Saml2_IdPMetadataParser.get_metadata('https://www.testshib.org/metadata/testshib-providers.xml')
+ except URLError:
+ data = self.file_contents(join(self.data_path, 'metadata', 'testshib-providers.xml'))
self.assertTrue(data is not None and data is not {})
def testParseRemote(self):
@@ -51,10 +62,29 @@ def testParseRemote(self):
with self.assertRaises(Exception):
data = OneLogin_Saml2_IdPMetadataParser.parse_remote('http://google.es')
- data = OneLogin_Saml2_IdPMetadataParser.parse_remote('https://www.testshib.org/metadata/testshib-providers.xml')
+ try:
+ data = OneLogin_Saml2_IdPMetadataParser.parse_remote('https://www.testshib.org/metadata/testshib-providers.xml')
+ except URLError:
+ xml = self.file_contents(join(self.data_path, 'metadata', 'testshib-providers.xml'))
+ data = OneLogin_Saml2_IdPMetadataParser.parse(xml)
+
self.assertTrue(data is not None and data is not {})
- expected_data = {'sp': {'NameIDFormat': 'urn:mace:shibboleth:1.0:nameIdentifier'}, 'idp': {'singleSignOnService': {'url': 'https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO'}, 'entityId': 'https://idp.testshib.org/idp/shibboleth'}}
- self.assertEqual(expected_data, data)
+ expected_settings_json = """
+ {
+ "sp": {
+ "NameIDFormat": "urn:mace:shibboleth:1.0:nameIdentifier"
+ },
+ "idp": {
+ "entityId": "https://idp.testshib.org/idp/shibboleth",
+ "singleSignOnService": {
+ "url": "https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO",
+ "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
+ }
+ }
+ }
+ """
+ expected_settings = json.loads(expected_settings_json)
+ self.assertEqual(expected_settings, data)
def testParse(self):
"""
@@ -69,29 +99,314 @@ def testParse(self):
xml_idp_metadata = self.file_contents(join(self.data_path, 'metadata', 'idp_metadata.xml'))
data = OneLogin_Saml2_IdPMetadataParser.parse(xml_idp_metadata)
- expected_data = {'sp': {'NameIDFormat': 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'}, 'idp': {'singleSignOnService': {'url': 'https://app.onelogin.com/trust/saml2/http-post/sso/383123'}, 'entityId': 'https://app.onelogin.com/saml/metadata/383123', 'x509cert': 'MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzET\nMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREwDwYD\nVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbTAeFw0xMzA2\nMDUxNzE2MjBaFw0xODA2MDUxNzE2MjBaMGcxCzAJBgNVBAYTAlVTMRMwEQYDVQQI\nDApDYWxpZm9ybmlhMRUwEwYDVQQHDAxTYW50YSBNb25pY2ExETAPBgNVBAoMCE9u\nZUxvZ2luMRkwFwYDVQQDDBBhcHAub25lbG9naW4uY29tMIIBIjANBgkqhkiG9w0B\nAQEFAAOCAQ8AMIIBCgKCAQEAse8rnep4qL2GmhH10pMQyJ2Jae+AQHyfgVjaQZ7Z\n0QQog5jX91vcJRSMi0XWJnUtOr6lF0dq1+yckjZ92wyLrH+7fvngNO1aV4Mjk9sT\ngf+iqMrae6y6fRxDt9PXrEFVjvd3vv7QTJf2FuIPy4vVP06Dt8EMkQIr8rmLmU0m\nTr1k2DkrdtdlCuNFTXuAu3QqfvNCRrRwfNObn9MP6JeOUdcGLJsBjGF8exfcN1SF\nzRF0JFr3dmOlx761zK5liD0T1sYWnDquatj/JD9fZMbKecBKni1NglH/LVd+b6aJ\nUAr5LulERULUjLqYJRKW31u91/4Qazdo9tbvwqyFxaoUrwIDAQABo4HUMIHRMAwG\nA1UdEwEB/wQCMAAwHQYDVR0OBBYEFPWcXvQSlTXnzZD2xziuoUvrrDedMIGRBgNV\nHSMEgYkwgYaAFPWcXvQSlTXnzZD2xziuoUvrrDedoWukaTBnMQswCQYDVQQGEwJV\nUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREw\nDwYDVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbYIBATAO\nBgNVHQ8BAf8EBAMCBPAwDQYJKoZIhvcNAQEFBQADggEBAB/8xe3rzqXQVxzHyAHu\nAuPa73ClDoL1cko0Fp8CGcqEIyj6Te9gx5z6wyfv+Lo8RFvBLlnB1lXqbC+fTGcV\ngG/4oKLJ5UwRFxInqpZPnOAudVNnd0PYOODn9FWs6u+OTIQIaIcPUv3MhB9lwHIJ\nsTk/bs9xcru5TPyLIxLLd6ib/pRceKH2mTkzUd0DYk9CQNXXeoGx/du5B9nh3ClP\nTbVakRzl3oswgI5MQIphYxkW70SopEh4kOFSRE1ND31NNIq1YrXlgtkguQBFsZWu\nQOPR6cEwFZzP0tHTYbI839WgxX6hfhIUTUz6mLqq4+3P4BG3+1OXeVDg63y8Uh78\n1sE='}}
- self.assertEqual(expected_data, data)
+
+ # W/o further specification, expect to get the redirect binding SSO
+ # URL extracted.
+ expected_settings_json = """
+ {
+ "idp": {
+ "singleSignOnService": {
+ "url": "https://app.onelogin.com/trust/saml2/http-post/sso/383123",
+ "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
+ },
+ "x509cert": "MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzET\\nMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREwDwYD\\nVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbTAeFw0xMzA2\\nMDUxNzE2MjBaFw0xODA2MDUxNzE2MjBaMGcxCzAJBgNVBAYTAlVTMRMwEQYDVQQI\\nDApDYWxpZm9ybmlhMRUwEwYDVQQHDAxTYW50YSBNb25pY2ExETAPBgNVBAoMCE9u\\nZUxvZ2luMRkwFwYDVQQDDBBhcHAub25lbG9naW4uY29tMIIBIjANBgkqhkiG9w0B\\nAQEFAAOCAQ8AMIIBCgKCAQEAse8rnep4qL2GmhH10pMQyJ2Jae+AQHyfgVjaQZ7Z\\n0QQog5jX91vcJRSMi0XWJnUtOr6lF0dq1+yckjZ92wyLrH+7fvngNO1aV4Mjk9sT\\ngf+iqMrae6y6fRxDt9PXrEFVjvd3vv7QTJf2FuIPy4vVP06Dt8EMkQIr8rmLmU0m\\nTr1k2DkrdtdlCuNFTXuAu3QqfvNCRrRwfNObn9MP6JeOUdcGLJsBjGF8exfcN1SF\\nzRF0JFr3dmOlx761zK5liD0T1sYWnDquatj/JD9fZMbKecBKni1NglH/LVd+b6aJ\\nUAr5LulERULUjLqYJRKW31u91/4Qazdo9tbvwqyFxaoUrwIDAQABo4HUMIHRMAwG\\nA1UdEwEB/wQCMAAwHQYDVR0OBBYEFPWcXvQSlTXnzZD2xziuoUvrrDedMIGRBgNV\\nHSMEgYkwgYaAFPWcXvQSlTXnzZD2xziuoUvrrDedoWukaTBnMQswCQYDVQQGEwJV\\nUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREw\\nDwYDVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbYIBATAO\\nBgNVHQ8BAf8EBAMCBPAwDQYJKoZIhvcNAQEFBQADggEBAB/8xe3rzqXQVxzHyAHu\\nAuPa73ClDoL1cko0Fp8CGcqEIyj6Te9gx5z6wyfv+Lo8RFvBLlnB1lXqbC+fTGcV\\ngG/4oKLJ5UwRFxInqpZPnOAudVNnd0PYOODn9FWs6u+OTIQIaIcPUv3MhB9lwHIJ\\nsTk/bs9xcru5TPyLIxLLd6ib/pRceKH2mTkzUd0DYk9CQNXXeoGx/du5B9nh3ClP\\nTbVakRzl3oswgI5MQIphYxkW70SopEh4kOFSRE1ND31NNIq1YrXlgtkguQBFsZWu\\nQOPR6cEwFZzP0tHTYbI839WgxX6hfhIUTUz6mLqq4+3P4BG3+1OXeVDg63y8Uh78\\n1sE=",
+ "entityId": "https://app.onelogin.com/saml/metadata/383123"
+ },
+ "sp": {
+ "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
+ }
+ }
+ """
+ expected_settings = json.loads(expected_settings_json)
+ self.assertEqual(expected_settings, data)
+
+ def test_parse_testshib_required_binding_sso_redirect(self):
+ """
+ Test with testshib metadata.
+ Especially test extracting SSO with REDIRECT binding.
+ Note that the testshib metadata does not contain an SLO specification
+ in the first tag.
+ """
+ expected_settings_json = """
+ {
+ "sp": {
+ "NameIDFormat": "urn:mace:shibboleth:1.0:nameIdentifier"
+ },
+ "idp": {
+ "entityId": "https://idp.testshib.org/idp/shibboleth",
+ "singleSignOnService": {
+ "url": "https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO",
+ "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
+ }
+ }
+ }
+ """
+ try:
+ xmldoc = OneLogin_Saml2_IdPMetadataParser.get_metadata(
+ 'https://www.testshib.org/metadata/testshib-providers.xml')
+ except Exception:
+ xmldoc = self.file_contents(join(self.data_path, 'metadata', 'testshib-providers.xml'))
+
+ # Parse, require SSO REDIRECT binding, implicitly.
+ settings1 = OneLogin_Saml2_IdPMetadataParser.parse(xmldoc)
+ # Parse, require SSO REDIRECT binding, explicitly.
+ settings2 = OneLogin_Saml2_IdPMetadataParser.parse(
+ xmldoc,
+ required_sso_binding=OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT
+ )
+ expected_settings = json.loads(expected_settings_json)
+ self.assertEqual(expected_settings, settings1)
+ self.assertEqual(expected_settings, settings2)
+
+ def test_parse_testshib_required_binding_sso_post(self):
+ """
+ Test with testshib metadata.
+ Especially test extracting SSO with POST binding.
+ """
+ expected_settings_json = """
+ {
+ "sp": {
+ "NameIDFormat": "urn:mace:shibboleth:1.0:nameIdentifier"
+ },
+ "idp": {
+ "entityId": "https://idp.testshib.org/idp/shibboleth",
+ "singleSignOnService": {
+ "url": "https://idp.testshib.org/idp/profile/SAML2/POST/SSO",
+ "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
+ }
+ }
+ }
+ """
+ try:
+ xmldoc = OneLogin_Saml2_IdPMetadataParser.get_metadata(
+ 'https://www.testshib.org/metadata/testshib-providers.xml')
+ except URLError:
+ xmldoc = self.file_contents(join(self.data_path, 'metadata', 'testshib-providers.xml'))
+
+ # Parse, require POST binding.
+ settings = OneLogin_Saml2_IdPMetadataParser.parse(
+ xmldoc,
+ required_sso_binding=OneLogin_Saml2_Constants.BINDING_HTTP_POST
+ )
+ expected_settings = json.loads(expected_settings_json)
+ self.assertEqual(expected_settings, settings)
+
+ def test_parse_required_binding_all(self):
+ """
+ Test all combinations of the `require_slo_binding` and
+ `require_sso_binding` parameters.
+ Note: IdP metadata contains a single logout (SLO)
+ service and does not specify any endpoint for the POST binding.
+ """
+ expected_settings_json = """
+ {
+ "sp": {
+ "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
+ },
+ "idp": {
+ "entityId": "urn:example:idp",
+ "x509cert": "MIIDPDCCAiQCCQDydJgOlszqbzANBgkqhkiG9w0BAQUFADBgMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzEQMA4GA1UEChMHSmFua3lDbzESMBAGA1UEAxMJbG9jYWxob3N0MB4XDTE0MDMxMjE5NDYzM1oXDTI3MTExOTE5NDYzM1owYDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xEDAOBgNVBAoTB0phbmt5Q28xEjAQBgNVBAMTCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMGvJpRTTasRUSPqcbqCG+ZnTAurnu0vVpIG9lzExnh11o/BGmzu7lB+yLHcEdwrKBBmpepDBPCYxpVajvuEhZdKFx/Fdy6j5mH3rrW0Bh/zd36CoUNjbbhHyTjeM7FN2yF3u9lcyubuvOzr3B3gX66IwJlU46+wzcQVhSOlMk2tXR+fIKQExFrOuK9tbX3JIBUqItpI+HnAow509CnM134svw8PTFLkR6/CcMqnDfDK1m993PyoC1Y+N4X9XkhSmEQoAlAHPI5LHrvuujM13nvtoVYvKYoj7ScgumkpWNEvX652LfXOnKYlkB8ZybuxmFfIkzedQrbJsyOhfL03cMECAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAeHwzqwnzGEkxjzSD47imXaTqtYyETZow7XwBc0ZaFS50qRFJUgKTAmKS1xQBP/qHpStsROT35DUxJAE6NY1Kbq3ZbCuhGoSlY0L7VzVT5tpu4EY8+Dq/u2EjRmmhoL7UkskvIZ2n1DdERtd+YUMTeqYl9co43csZwDno/IKomeN5qaPc39IZjikJ+nUC6kPFKeu/3j9rgHNlRtocI6S1FdtFz9OZMQlpr0JbUt2T3xS/YoQJn6coDmJL5GTiiKM6cOe+Ur1VwzS1JEDbSS2TWWhzq8ojLdrotYLGd9JOsoQhElmz+tMfCFQUFLExinPAyy7YHlSiVX13QH2XTu/iQQ==",
+ "singleSignOnService": {
+ "url": "http://idp.example.com",
+ "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
+ },
+ "singleLogoutService": {
+ "url": "http://idp.example.com/logout",
+ "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
+ }
+ }
+ }
+ """
+ xmldoc = self.file_contents(join(self.data_path, 'metadata', 'idp_metadata2.xml'))
+
+ expected_settings = json.loads(expected_settings_json)
+
+ # Parse, require SLO and SSO REDIRECT binding, implicitly.
+ settings1 = OneLogin_Saml2_IdPMetadataParser.parse(xmldoc)
+
+ # Parse, require SLO and SSO REDIRECT binding, explicitly.
+ settings2 = OneLogin_Saml2_IdPMetadataParser.parse(
+ xmldoc,
+ required_sso_binding=OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT,
+ required_slo_binding=OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT
+ )
+ expected_settings1_2 = deepcopy(expected_settings)
+ self.assertEqual(expected_settings1_2, settings1)
+ self.assertEqual(expected_settings1_2, settings2)
+
+ settings3 = OneLogin_Saml2_IdPMetadataParser.parse(
+ xmldoc,
+ required_sso_binding=OneLogin_Saml2_Constants.BINDING_HTTP_POST,
+ required_slo_binding=OneLogin_Saml2_Constants.BINDING_HTTP_POST
+ )
+
+ expected_settings3 = deepcopy(expected_settings)
+ del expected_settings3['idp']['singleLogoutService']
+ del expected_settings3['idp']['singleSignOnService']
+ self.assertEqual(expected_settings3, settings3)
+
+ settings4 = OneLogin_Saml2_IdPMetadataParser.parse(
+ xmldoc,
+ required_sso_binding=OneLogin_Saml2_Constants.BINDING_HTTP_POST,
+ required_slo_binding=OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT
+ )
+ settings5 = OneLogin_Saml2_IdPMetadataParser.parse(
+ xmldoc,
+ required_sso_binding=OneLogin_Saml2_Constants.BINDING_HTTP_POST
+ )
+ expected_settings4_5 = deepcopy(expected_settings)
+ del expected_settings4_5['idp']['singleSignOnService']
+ self.assertEqual(expected_settings4_5, settings4)
+ self.assertEqual(expected_settings4_5, settings5)
+
+ settings6 = OneLogin_Saml2_IdPMetadataParser.parse(
+ xmldoc,
+ required_sso_binding=OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT,
+ required_slo_binding=OneLogin_Saml2_Constants.BINDING_HTTP_POST
+ )
+ settings7 = OneLogin_Saml2_IdPMetadataParser.parse(
+ xmldoc,
+ required_slo_binding=OneLogin_Saml2_Constants.BINDING_HTTP_POST
+ )
+ expected_settings6_7 = deepcopy(expected_settings)
+ del expected_settings6_7['idp']['singleLogoutService']
+ self.assertEqual(expected_settings6_7, settings6)
+ self.assertEqual(expected_settings6_7, settings7)
def testMergeSettings(self):
"""
Tests the merge_settings method of the OneLogin_Saml2_IdPMetadataParser
"""
- with self.assertRaises(AttributeError):
+ with self.assertRaises(TypeError):
settings_result = OneLogin_Saml2_IdPMetadataParser.merge_settings(None, {})
with self.assertRaises(TypeError):
settings_result = OneLogin_Saml2_IdPMetadataParser.merge_settings({}, None)
xml_idp_metadata = self.file_contents(join(self.data_path, 'metadata', 'idp_metadata.xml'))
+
+ # Parse XML metadata.
data = OneLogin_Saml2_IdPMetadataParser.parse(xml_idp_metadata)
+
+ # Read base settings.
settings = self.loadSettingsJSON()
+
+ # Merge settings from XML metadata into base settings,
+ # let XML metadata have priority if there are conflicting
+ # attributes.
settings_result = OneLogin_Saml2_IdPMetadataParser.merge_settings(settings, data)
- expected_data = {u'sp': {'NameIDFormat': 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress'}, u'idp': {'singleSignOnService': {'url': 'https://app.onelogin.com/trust/saml2/http-post/sso/383123'}, 'entityId': 'https://app.onelogin.com/saml/metadata/383123', 'x509cert': 'MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzET\nMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREwDwYD\nVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbTAeFw0xMzA2\nMDUxNzE2MjBaFw0xODA2MDUxNzE2MjBaMGcxCzAJBgNVBAYTAlVTMRMwEQYDVQQI\nDApDYWxpZm9ybmlhMRUwEwYDVQQHDAxTYW50YSBNb25pY2ExETAPBgNVBAoMCE9u\nZUxvZ2luMRkwFwYDVQQDDBBhcHAub25lbG9naW4uY29tMIIBIjANBgkqhkiG9w0B\nAQEFAAOCAQ8AMIIBCgKCAQEAse8rnep4qL2GmhH10pMQyJ2Jae+AQHyfgVjaQZ7Z\n0QQog5jX91vcJRSMi0XWJnUtOr6lF0dq1+yckjZ92wyLrH+7fvngNO1aV4Mjk9sT\ngf+iqMrae6y6fRxDt9PXrEFVjvd3vv7QTJf2FuIPy4vVP06Dt8EMkQIr8rmLmU0m\nTr1k2DkrdtdlCuNFTXuAu3QqfvNCRrRwfNObn9MP6JeOUdcGLJsBjGF8exfcN1SF\nzRF0JFr3dmOlx761zK5liD0T1sYWnDquatj/JD9fZMbKecBKni1NglH/LVd+b6aJ\nUAr5LulERULUjLqYJRKW31u91/4Qazdo9tbvwqyFxaoUrwIDAQABo4HUMIHRMAwG\nA1UdEwEB/wQCMAAwHQYDVR0OBBYEFPWcXvQSlTXnzZD2xziuoUvrrDedMIGRBgNV\nHSMEgYkwgYaAFPWcXvQSlTXnzZD2xziuoUvrrDedoWukaTBnMQswCQYDVQQGEwJV\nUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREw\nDwYDVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbYIBATAO\nBgNVHQ8BAf8EBAMCBPAwDQYJKoZIhvcNAQEFBQADggEBAB/8xe3rzqXQVxzHyAHu\nAuPa73ClDoL1cko0Fp8CGcqEIyj6Te9gx5z6wyfv+Lo8RFvBLlnB1lXqbC+fTGcV\ngG/4oKLJ5UwRFxInqpZPnOAudVNnd0PYOODn9FWs6u+OTIQIaIcPUv3MhB9lwHIJ\nsTk/bs9xcru5TPyLIxLLd6ib/pRceKH2mTkzUd0DYk9CQNXXeoGx/du5B9nh3ClP\nTbVakRzl3oswgI5MQIphYxkW70SopEh4kOFSRE1ND31NNIq1YrXlgtkguQBFsZWu\nQOPR6cEwFZzP0tHTYbI839WgxX6hfhIUTUz6mLqq4+3P4BG3+1OXeVDg63y8Uh78\n1sE='}, u'strict': False, u'contactPerson': {u'technical': {u'givenName': u'technical_name', u'emailAddress': u'technical@example.com'}, u'support': {u'givenName': u'support_name', u'emailAddress': u'support@example.com'}}, u'debug': False, u'organization': {u'en-US': {u'url': u'http://sp.example.com', u'displayname': u'SP test', u'name': u'sp_test'}}, u'security': {u'signMetadata': False, u'wantAssertionsSigned': False, u'authnRequestsSigned': False}, u'custom_base_path': u'../../../tests/data/customPath/'}
- self.assertEqual(expected_data, settings_result)
- expected_data2 = {'sp': {u'singleLogoutService': {u'url': u'http://stuff.com/endpoints/endpoints/sls.php'}, u'assertionConsumerService': {u'url': u'http://stuff.com/endpoints/endpoints/acs.php'}, u'entityId': u'http://stuff.com/endpoints/metadata.php', u'NameIDFormat': u'urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified'}, 'idp': {u'singleLogoutService': {u'url': u'http://idp.example.com/SingleLogoutService.php'}, u'entityId': u'http://idp.example.com/', u'x509cert': u'MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo', u'singleSignOnService': {u'url': u'http://idp.example.com/SSOService.php'}}, u'strict': False, u'contactPerson': {u'technical': {u'givenName': u'technical_name', u'emailAddress': u'technical@example.com'}, u'support': {u'givenName': u'support_name', u'emailAddress': u'support@example.com'}}, u'debug': False, u'organization': {u'en-US': {u'url': u'http://sp.example.com', u'displayname': u'SP test', u'name': u'sp_test'}}, u'security': {u'signMetadata': False, u'wantAssertionsSigned': False, u'authnRequestsSigned': False}, u'custom_base_path': u'../../../tests/data/customPath/'}
+ # Generate readable JSON representation:
+ # print("%s" % json.dumps(settings_result, indent=2).replace(r'\n', r'\\n'))
+
+ expected_settings_json = """
+ {
+ "custom_base_path": "../../../tests/data/customPath/",
+ "contactPerson": {
+ "support": {
+ "emailAddress": "support@example.com",
+ "givenName": "support_name"
+ },
+ "technical": {
+ "emailAddress": "technical@example.com",
+ "givenName": "technical_name"
+ }
+ },
+ "idp": {
+ "singleSignOnService": {
+ "url": "https://app.onelogin.com/trust/saml2/http-post/sso/383123",
+ "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
+ },
+ "entityId": "https://app.onelogin.com/saml/metadata/383123",
+ "singleLogoutService": {
+ "url": "http://idp.example.com/SingleLogoutService.php"
+ },
+ "x509cert": "MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzET\\nMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREwDwYD\\nVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbTAeFw0xMzA2\\nMDUxNzE2MjBaFw0xODA2MDUxNzE2MjBaMGcxCzAJBgNVBAYTAlVTMRMwEQYDVQQI\\nDApDYWxpZm9ybmlhMRUwEwYDVQQHDAxTYW50YSBNb25pY2ExETAPBgNVBAoMCE9u\\nZUxvZ2luMRkwFwYDVQQDDBBhcHAub25lbG9naW4uY29tMIIBIjANBgkqhkiG9w0B\\nAQEFAAOCAQ8AMIIBCgKCAQEAse8rnep4qL2GmhH10pMQyJ2Jae+AQHyfgVjaQZ7Z\\n0QQog5jX91vcJRSMi0XWJnUtOr6lF0dq1+yckjZ92wyLrH+7fvngNO1aV4Mjk9sT\\ngf+iqMrae6y6fRxDt9PXrEFVjvd3vv7QTJf2FuIPy4vVP06Dt8EMkQIr8rmLmU0m\\nTr1k2DkrdtdlCuNFTXuAu3QqfvNCRrRwfNObn9MP6JeOUdcGLJsBjGF8exfcN1SF\\nzRF0JFr3dmOlx761zK5liD0T1sYWnDquatj/JD9fZMbKecBKni1NglH/LVd+b6aJ\\nUAr5LulERULUjLqYJRKW31u91/4Qazdo9tbvwqyFxaoUrwIDAQABo4HUMIHRMAwG\\nA1UdEwEB/wQCMAAwHQYDVR0OBBYEFPWcXvQSlTXnzZD2xziuoUvrrDedMIGRBgNV\\nHSMEgYkwgYaAFPWcXvQSlTXnzZD2xziuoUvrrDedoWukaTBnMQswCQYDVQQGEwJV\\nUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREw\\nDwYDVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbYIBATAO\\nBgNVHQ8BAf8EBAMCBPAwDQYJKoZIhvcNAQEFBQADggEBAB/8xe3rzqXQVxzHyAHu\\nAuPa73ClDoL1cko0Fp8CGcqEIyj6Te9gx5z6wyfv+Lo8RFvBLlnB1lXqbC+fTGcV\\ngG/4oKLJ5UwRFxInqpZPnOAudVNnd0PYOODn9FWs6u+OTIQIaIcPUv3MhB9lwHIJ\\nsTk/bs9xcru5TPyLIxLLd6ib/pRceKH2mTkzUd0DYk9CQNXXeoGx/du5B9nh3ClP\\nTbVakRzl3oswgI5MQIphYxkW70SopEh4kOFSRE1ND31NNIq1YrXlgtkguQBFsZWu\\nQOPR6cEwFZzP0tHTYbI839WgxX6hfhIUTUz6mLqq4+3P4BG3+1OXeVDg63y8Uh78\\n1sE="
+ },
+ "sp": {
+ "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
+ "entityId": "http://stuff.com/endpoints/metadata.php",
+ "assertionConsumerService": {
+ "url": "http://stuff.com/endpoints/endpoints/acs.php"
+ },
+ "singleLogoutService": {
+ "url": "http://stuff.com/endpoints/endpoints/sls.php"
+ }
+ },
+ "security": {
+ "wantAssertionsSigned": false,
+ "authnRequestsSigned": false,
+ "signMetadata": false
+ },
+ "debug": false,
+ "organization": {
+ "en-US": {
+ "displayname": "SP test",
+ "url": "http://sp.example.com",
+ "name": "sp_test"
+ }
+ },
+ "strict": false
+ }
+ """
+ expected_settings = json.loads(expected_settings_json)
+ self.assertEqual(expected_settings, settings_result)
+
+ # Commute merge operation. As the order determines which settings
+ # dictionary has priority, here we expect a different result.
settings_result2 = OneLogin_Saml2_IdPMetadataParser.merge_settings(data, settings)
- self.assertEqual(expected_data2, settings_result2)
+ expected_settings2_json = """
+ {
+ "debug": false,
+ "idp": {
+ "singleLogoutService": {
+ "url": "http://idp.example.com/SingleLogoutService.php"
+ },
+ "singleSignOnService": {
+ "url": "http://idp.example.com/SSOService.php",
+ "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
+ },
+ "entityId": "http://idp.example.com/",
+ "x509cert": "MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo"
+ },
+ "security": {
+ "authnRequestsSigned": false,
+ "wantAssertionsSigned": false,
+ "signMetadata": false
+ },
+ "contactPerson": {
+ "technical": {
+ "emailAddress": "technical@example.com",
+ "givenName": "technical_name"
+ },
+ "support": {
+ "emailAddress": "support@example.com",
+ "givenName": "support_name"
+ }
+ },
+ "strict": false,
+ "sp": {
+ "singleLogoutService": {
+ "url": "http://stuff.com/endpoints/endpoints/sls.php"
+ },
+ "assertionConsumerService": {
+ "url": "http://stuff.com/endpoints/endpoints/acs.php"
+ },
+ "entityId": "http://stuff.com/endpoints/metadata.php",
+ "NameIDFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified"
+ },
+ "custom_base_path": "../../../tests/data/customPath/",
+ "organization": {
+ "en-US": {
+ "displayname": "SP test",
+ "url": "http://sp.example.com",
+ "name": "sp_test"
+ }
+ }
+ }
+ """
+ expected_settings2 = json.loads(expected_settings2_json)
+ self.assertEqual(expected_settings2, settings_result2)
if __name__ == '__main__':
From 574b90ed5b6497e65e9cff3c0e69d63f26dd9c7b Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Sat, 14 May 2016 13:12:19 +0200
Subject: [PATCH 013/255] Forgot xml files for the tests
---
tests/data/metadata/idp_metadata2.xml | 1 +
tests/data/metadata/testshib-providers.xml | 311 +++++++++++++++++++++
2 files changed, 312 insertions(+)
create mode 100644 tests/data/metadata/idp_metadata2.xml
create mode 100644 tests/data/metadata/testshib-providers.xml
diff --git a/tests/data/metadata/idp_metadata2.xml b/tests/data/metadata/idp_metadata2.xml
new file mode 100644
index 00000000..0b19f62a
--- /dev/null
+++ b/tests/data/metadata/idp_metadata2.xml
@@ -0,0 +1 @@
+ MIIDPDCCAiQCCQDydJgOlszqbzANBgkqhkiG9w0BAQUFADBgMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzEQMA4GA1UEChMHSmFua3lDbzESMBAGA1UEAxMJbG9jYWxob3N0MB4XDTE0MDMxMjE5NDYzM1oXDTI3MTExOTE5NDYzM1owYDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xEDAOBgNVBAoTB0phbmt5Q28xEjAQBgNVBAMTCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMGvJpRTTasRUSPqcbqCG+ZnTAurnu0vVpIG9lzExnh11o/BGmzu7lB+yLHcEdwrKBBmpepDBPCYxpVajvuEhZdKFx/Fdy6j5mH3rrW0Bh/zd36CoUNjbbhHyTjeM7FN2yF3u9lcyubuvOzr3B3gX66IwJlU46+wzcQVhSOlMk2tXR+fIKQExFrOuK9tbX3JIBUqItpI+HnAow509CnM134svw8PTFLkR6/CcMqnDfDK1m993PyoC1Y+N4X9XkhSmEQoAlAHPI5LHrvuujM13nvtoVYvKYoj7ScgumkpWNEvX652LfXOnKYlkB8ZybuxmFfIkzedQrbJsyOhfL03cMECAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAeHwzqwnzGEkxjzSD47imXaTqtYyETZow7XwBc0ZaFS50qRFJUgKTAmKS1xQBP/qHpStsROT35DUxJAE6NY1Kbq3ZbCuhGoSlY0L7VzVT5tpu4EY8+Dq/u2EjRmmhoL7UkskvIZ2n1DdERtd+YUMTeqYl9co43csZwDno/IKomeN5qaPc39IZjikJ+nUC6kPFKeu/3j9rgHNlRtocI6S1FdtFz9OZMQlpr0JbUt2T3xS/YoQJn6coDmJL5GTiiKM6cOe+Ur1VwzS1JEDbSS2TWWhzq8ojLdrotYLGd9JOsoQhElmz+tMfCFQUFLExinPAyy7YHlSiVX13QH2XTu/iQQ== urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress urn:oasis:names:tc:SAML:2.0:nameid-format:persistent urn:oasis:names:tc:SAML:2.0:nameid-format:transient
\ No newline at end of file
diff --git a/tests/data/metadata/testshib-providers.xml b/tests/data/metadata/testshib-providers.xml
new file mode 100644
index 00000000..c00a1b77
--- /dev/null
+++ b/tests/data/metadata/testshib-providers.xml
@@ -0,0 +1,311 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ testshib.org
+
+ TestShib Test IdP
+ TestShib IdP. Use this as a source of attributes
+ for your test SP.
+ https://www.testshib.org/testshibtwo.jpg
+
+
+
+
+
+
+
+ MIIEDjCCAvagAwIBAgIBADANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzEV
+ MBMGA1UECBMMUGVubnN5bHZhbmlhMRMwEQYDVQQHEwpQaXR0c2J1cmdoMREwDwYD
+ VQQKEwhUZXN0U2hpYjEZMBcGA1UEAxMQaWRwLnRlc3RzaGliLm9yZzAeFw0wNjA4
+ MzAyMTEyMjVaFw0xNjA4MjcyMTEyMjVaMGcxCzAJBgNVBAYTAlVTMRUwEwYDVQQI
+ EwxQZW5uc3lsdmFuaWExEzARBgNVBAcTClBpdHRzYnVyZ2gxETAPBgNVBAoTCFRl
+ c3RTaGliMRkwFwYDVQQDExBpZHAudGVzdHNoaWIub3JnMIIBIjANBgkqhkiG9w0B
+ AQEFAAOCAQ8AMIIBCgKCAQEArYkCGuTmJp9eAOSGHwRJo1SNatB5ZOKqDM9ysg7C
+ yVTDClcpu93gSP10nH4gkCZOlnESNgttg0r+MqL8tfJC6ybddEFB3YBo8PZajKSe
+ 3OQ01Ow3yT4I+Wdg1tsTpSge9gEz7SrC07EkYmHuPtd71CHiUaCWDv+xVfUQX0aT
+ NPFmDixzUjoYzbGDrtAyCqA8f9CN2txIfJnpHE6q6CmKcoLADS4UrNPlhHSzd614
+ kR/JYiks0K4kbRqCQF0Dv0P5Di+rEfefC6glV8ysC8dB5/9nb0yh/ojRuJGmgMWH
+ gWk6h0ihjihqiu4jACovUZ7vVOCgSE5Ipn7OIwqd93zp2wIDAQABo4HEMIHBMB0G
+ A1UdDgQWBBSsBQ869nh83KqZr5jArr4/7b+QazCBkQYDVR0jBIGJMIGGgBSsBQ86
+ 9nh83KqZr5jArr4/7b+Qa6FrpGkwZzELMAkGA1UEBhMCVVMxFTATBgNVBAgTDFBl
+ bm5zeWx2YW5pYTETMBEGA1UEBxMKUGl0dHNidXJnaDERMA8GA1UEChMIVGVzdFNo
+ aWIxGTAXBgNVBAMTEGlkcC50ZXN0c2hpYi5vcmeCAQAwDAYDVR0TBAUwAwEB/zAN
+ BgkqhkiG9w0BAQUFAAOCAQEAjR29PhrCbk8qLN5MFfSVk98t3CT9jHZoYxd8QMRL
+ I4j7iYQxXiGJTT1FXs1nd4Rha9un+LqTfeMMYqISdDDI6tv8iNpkOAvZZUosVkUo
+ 93pv1T0RPz35hcHHYq2yee59HJOco2bFlcsH8JBXRSRrJ3Q7Eut+z9uo80JdGNJ4
+ /SJy5UorZ8KazGj16lfJhOBXldgrhppQBb0Nq6HKHguqmwRfJ+WkxemZXzhediAj
+ Geka8nz8JjwxpUjAiSWYKLtJhGEaTqCYxCCX2Dw+dOTqUzHOZ7WKv4JXPK5G/Uhr
+ 8K/qhmFT2nIQi538n6rVYLeWj8Bbnl+ev0peYzxFyF5sQA==
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ urn:mace:shibboleth:1.0:nameIdentifier
+ urn:oasis:names:tc:SAML:2.0:nameid-format:transient
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ MIIEDjCCAvagAwIBAgIBADANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzEV
+ MBMGA1UECBMMUGVubnN5bHZhbmlhMRMwEQYDVQQHEwpQaXR0c2J1cmdoMREwDwYD
+ VQQKEwhUZXN0U2hpYjEZMBcGA1UEAxMQaWRwLnRlc3RzaGliLm9yZzAeFw0wNjA4
+ MzAyMTEyMjVaFw0xNjA4MjcyMTEyMjVaMGcxCzAJBgNVBAYTAlVTMRUwEwYDVQQI
+ EwxQZW5uc3lsdmFuaWExEzARBgNVBAcTClBpdHRzYnVyZ2gxETAPBgNVBAoTCFRl
+ c3RTaGliMRkwFwYDVQQDExBpZHAudGVzdHNoaWIub3JnMIIBIjANBgkqhkiG9w0B
+ AQEFAAOCAQ8AMIIBCgKCAQEArYkCGuTmJp9eAOSGHwRJo1SNatB5ZOKqDM9ysg7C
+ yVTDClcpu93gSP10nH4gkCZOlnESNgttg0r+MqL8tfJC6ybddEFB3YBo8PZajKSe
+ 3OQ01Ow3yT4I+Wdg1tsTpSge9gEz7SrC07EkYmHuPtd71CHiUaCWDv+xVfUQX0aT
+ NPFmDixzUjoYzbGDrtAyCqA8f9CN2txIfJnpHE6q6CmKcoLADS4UrNPlhHSzd614
+ kR/JYiks0K4kbRqCQF0Dv0P5Di+rEfefC6glV8ysC8dB5/9nb0yh/ojRuJGmgMWH
+ gWk6h0ihjihqiu4jACovUZ7vVOCgSE5Ipn7OIwqd93zp2wIDAQABo4HEMIHBMB0G
+ A1UdDgQWBBSsBQ869nh83KqZr5jArr4/7b+QazCBkQYDVR0jBIGJMIGGgBSsBQ86
+ 9nh83KqZr5jArr4/7b+Qa6FrpGkwZzELMAkGA1UEBhMCVVMxFTATBgNVBAgTDFBl
+ bm5zeWx2YW5pYTETMBEGA1UEBxMKUGl0dHNidXJnaDERMA8GA1UEChMIVGVzdFNo
+ aWIxGTAXBgNVBAMTEGlkcC50ZXN0c2hpYi5vcmeCAQAwDAYDVR0TBAUwAwEB/zAN
+ BgkqhkiG9w0BAQUFAAOCAQEAjR29PhrCbk8qLN5MFfSVk98t3CT9jHZoYxd8QMRL
+ I4j7iYQxXiGJTT1FXs1nd4Rha9un+LqTfeMMYqISdDDI6tv8iNpkOAvZZUosVkUo
+ 93pv1T0RPz35hcHHYq2yee59HJOco2bFlcsH8JBXRSRrJ3Q7Eut+z9uo80JdGNJ4
+ /SJy5UorZ8KazGj16lfJhOBXldgrhppQBb0Nq6HKHguqmwRfJ+WkxemZXzhediAj
+ Geka8nz8JjwxpUjAiSWYKLtJhGEaTqCYxCCX2Dw+dOTqUzHOZ7WKv4JXPK5G/Uhr
+ 8K/qhmFT2nIQi538n6rVYLeWj8Bbnl+ev0peYzxFyF5sQA==
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ urn:mace:shibboleth:1.0:nameIdentifier
+ urn:oasis:names:tc:SAML:2.0:nameid-format:transient
+
+
+
+
+ TestShib Two Identity Provider
+ TestShib Two
+ http://www.testshib.org/testshib-two/
+
+
+ Nate
+ Klingenstein
+ ndk@internet2.edu
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ TestShib Test SP
+ TestShib SP. Log into this to test your machine.
+ Once logged in check that all attributes that you expected have been
+ released.
+ https://www.testshib.org/testshibtwo.jpg
+
+
+
+
+
+
+
+ MIIEPjCCAyagAwIBAgIBADANBgkqhkiG9w0BAQUFADB3MQswCQYDVQQGEwJVUzEV
+ MBMGA1UECBMMUGVubnN5bHZhbmlhMRMwEQYDVQQHEwpQaXR0c2J1cmdoMSIwIAYD
+ VQQKExlUZXN0U2hpYiBTZXJ2aWNlIFByb3ZpZGVyMRgwFgYDVQQDEw9zcC50ZXN0
+ c2hpYi5vcmcwHhcNMDYwODMwMjEyNDM5WhcNMTYwODI3MjEyNDM5WjB3MQswCQYD
+ VQQGEwJVUzEVMBMGA1UECBMMUGVubnN5bHZhbmlhMRMwEQYDVQQHEwpQaXR0c2J1
+ cmdoMSIwIAYDVQQKExlUZXN0U2hpYiBTZXJ2aWNlIFByb3ZpZGVyMRgwFgYDVQQD
+ Ew9zcC50ZXN0c2hpYi5vcmcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
+ AQDJyR6ZP6MXkQ9z6RRziT0AuCabDd3x1m7nLO9ZRPbr0v1LsU+nnC363jO8nGEq
+ sqkgiZ/bSsO5lvjEt4ehff57ERio2Qk9cYw8XCgmYccVXKH9M+QVO1MQwErNobWb
+ AjiVkuhWcwLWQwTDBowfKXI87SA7KR7sFUymNx5z1aoRvk3GM++tiPY6u4shy8c7
+ vpWbVfisfTfvef/y+galxjPUQYHmegu7vCbjYP3On0V7/Ivzr+r2aPhp8egxt00Q
+ XpilNai12LBYV3Nv/lMsUzBeB7+CdXRVjZOHGuQ8mGqEbsj8MBXvcxIKbcpeK5Zi
+ JCVXPfarzuriM1G5y5QkKW+LAgMBAAGjgdQwgdEwHQYDVR0OBBYEFKB6wPDxwYrY
+ StNjU5P4b4AjBVQVMIGhBgNVHSMEgZkwgZaAFKB6wPDxwYrYStNjU5P4b4AjBVQV
+ oXukeTB3MQswCQYDVQQGEwJVUzEVMBMGA1UECBMMUGVubnN5bHZhbmlhMRMwEQYD
+ VQQHEwpQaXR0c2J1cmdoMSIwIAYDVQQKExlUZXN0U2hpYiBTZXJ2aWNlIFByb3Zp
+ ZGVyMRgwFgYDVQQDEw9zcC50ZXN0c2hpYi5vcmeCAQAwDAYDVR0TBAUwAwEB/zAN
+ BgkqhkiG9w0BAQUFAAOCAQEAc06Kgt7ZP6g2TIZgMbFxg6vKwvDL0+2dzF11Onpl
+ 5sbtkPaNIcj24lQ4vajCrrGKdzHXo9m54BzrdRJ7xDYtw0dbu37l1IZVmiZr12eE
+ Iay/5YMU+aWP1z70h867ZQ7/7Y4HW345rdiS6EW663oH732wSYNt9kr7/0Uer3KD
+ 9CuPuOidBacospDaFyfsaJruE99Kd6Eu/w5KLAGG+m0iqENCziDGzVA47TngKz2v
+ PVA+aokoOyoz3b53qeti77ijatSEoKjxheBWpO+eoJeGq/e49Um3M2ogIX/JAlMa
+ Inh+vYSYngQB2sx9LGkR9KHaMKNIGCDehk93Xla4pWJx1w==
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ urn:oasis:names:tc:SAML:2.0:nameid-format:transient
+ urn:mace:shibboleth:1.0:nameIdentifier
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ TestShib Two Service Provider
+ TestShib Two
+ http://www.testshib.org/testshib-two/
+
+
+ Nate
+ Klingenstein
+ ndk@internet2.edu
+
+
+
+
+
+
+
From 431e50475875dcdfe4833911dbff15c7b71c9b34 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Sat, 14 May 2016 19:26:27 +0200
Subject: [PATCH 014/255] Remove reference to wrong NameIDFormat:
urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified . Should be
urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified
---
README.md | 2 +-
demo-bottle/saml/settings.json | 2 +-
demo-django/saml/settings.json | 2 +-
demo-flask/saml/settings.json | 2 +-
.../logout_requests/invalids/invalid_issuer.xml | 2 +-
.../invalids/invalid_issuer.xml.base64 | 2 +-
.../logout_requests/invalids/not_after_failed.xml | 2 +-
.../invalids/not_after_failed.xml.base64 | 2 +-
tests/data/logout_requests/logout_request.xml | 2 +-
.../data/logout_requests/logout_request.xml.base64 | 2 +-
.../logout_request_deflated.xml.base64 | 2 +-
.../logout_request_with_sessionindex.xml | 2 +-
tests/data/metadata/expired_metadata_settings1.xml | 2 +-
.../data/metadata/metadata_bad_order_settings1.xml | 2 +-
tests/data/metadata/metadata_settings1.xml | 2 +-
.../data/metadata/no_expiration_mark_metadata.xml | 2 +-
.../data/metadata/noentity_metadata_settings1.xml | 2 +-
tests/data/metadata/unparsed_metadata.xml | 2 +-
.../responses/response_encrypted_nameid.xml.base64 | 2 +-
tests/settings/settings1.json | 2 +-
tests/settings/settings2.json | 2 +-
tests/settings/settings3.json | 2 +-
tests/settings/settings4.json | 2 +-
tests/src/OneLogin/saml2_tests/auth_test.py | 2 +-
.../saml2_tests/idp_metadata_parser_test.py | 2 +-
.../OneLogin/saml2_tests/logout_request_test.py | 2 +-
tests/src/OneLogin/saml2_tests/metadata_test.py | 4 ++--
tests/src/OneLogin/saml2_tests/response_test.py | 2 +-
tests/src/OneLogin/saml2_tests/settings_test.py | 6 +++---
tests/src/OneLogin/saml2_tests/utils_test.py | 14 +++++++-------
30 files changed, 39 insertions(+), 39 deletions(-)
diff --git a/README.md b/README.md
index 07b96dc8..ba73879c 100644
--- a/README.md
+++ b/README.md
@@ -252,7 +252,7 @@ This is the settings.json file:
// Specifies the constraints on the name identifier to be used to
// 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:2.0:nameid-format:unspecified",
+ "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified",
// Usually x509cert 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": "",
diff --git a/demo-bottle/saml/settings.json b/demo-bottle/saml/settings.json
index fdb13acd..7f861e97 100644
--- a/demo-bottle/saml/settings.json
+++ b/demo-bottle/saml/settings.json
@@ -11,7 +11,7 @@
"url": "https:///?sls",
"binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
},
- "NameIDFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified",
+ "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified",
"x509cert": "",
"privateKey": ""
},
diff --git a/demo-django/saml/settings.json b/demo-django/saml/settings.json
index cafd090b..391b91c1 100644
--- a/demo-django/saml/settings.json
+++ b/demo-django/saml/settings.json
@@ -11,7 +11,7 @@
"url": "https:///?sls",
"binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
},
- "NameIDFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified",
+ "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified",
"x509cert": "",
"privateKey": ""
},
diff --git a/demo-flask/saml/settings.json b/demo-flask/saml/settings.json
index 142911f1..ec40b674 100644
--- a/demo-flask/saml/settings.json
+++ b/demo-flask/saml/settings.json
@@ -11,7 +11,7 @@
"url": "https:///?sls",
"binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
},
- "NameIDFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified",
+ "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified",
"x509cert": "",
"privateKey": ""
},
diff --git a/tests/data/logout_requests/invalids/invalid_issuer.xml b/tests/data/logout_requests/invalids/invalid_issuer.xml
index d5bf6609..e1edabde 100644
--- a/tests/data/logout_requests/invalids/invalid_issuer.xml
+++ b/tests/data/logout_requests/invalids/invalid_issuer.xml
@@ -9,6 +9,6 @@
>
https://example.hello.com/access/saml
ONELOGIN_1e442c129e1f822c8096086a1103c5ee2c7cae1c
diff --git a/tests/data/logout_requests/invalids/invalid_issuer.xml.base64 b/tests/data/logout_requests/invalids/invalid_issuer.xml.base64
index 26912f3d..56de49b0 100644
--- a/tests/data/logout_requests/invalids/invalid_issuer.xml.base64
+++ b/tests/data/logout_requests/invalids/invalid_issuer.xml.base64
@@ -1 +1 @@
-jZPJbsIwEEDvfAXKncR2lhILgpBoq0gU2lL10EvlOpMSKbHTjFPx+XXSjQNB+GTZ4zdv7PFscajK8Sc0WGg1d6hLnEUymqGoypqv9btuzSN8tIBmbAMV8n5n7rSN4lpggVyJCpAbyXfLuzVnLuF1o42WunRG41Pjn3MeIxChMVZrgJOu5s52c73e3qabV0bDaSBllgsp/EjQIBQQxyQIWCazOHrLISI0pDDAev69AJt4KB1iC6lCI5SxcYT6E8omlDyRgPsx9+nLwMGVvbxCCdPz98bU3PPQtHnuSl15oLJaF8rg0QxLdOt9PcDbaLNV22aZG2g6EeZPSHiJSNIv92/L+2qapNNB6wMHUdUluHsoS917CSkBrYoNnnnHR44gG/tm6Wq8u+8mD60oi7zonC6inpC80U0lzPmu6FaKbJL3obxVWIPs0mYneMlfe1CwnSApi4HmU8bklMQRmdo+ocSXIQCTV1IAlT+lfhdm/4F34iMkXw==
+jZPfb4IwEMffTfwfDO9CW8BJoxgTt8XEyTaXPexl6coxSaBlXFn881dwP3yQxT411+v3+7n2brY4lMXoE2rMtZo71CXOIh4OZijKouIb/a4b8wgfDaAZ2UyFvDuZO02tuBaYI1eiBORG8t3ybsOZS3hVa6OlLpzhYHRu/Qn9ryMQoTYWrE9ovZo7yfZ6k9yut6+MhtNAyjQTUvgTQYNQQBSRIGCpTKPJWwYTQkMKfWLPP49grXsNERtYKzRCGZtIqD+mbEzJEwm4H3GfvvTdXNkXzJUwncPemIp7Hpomy1ypSw9UWulcGTzZYYFuta/6BLfaJCqpl5mBukVh/piEF6HEx3j3x7yrqI5bIrRIcBBlVYC7h6LQHZqQEtDS2OSZd3rlVGVr/269Gu3u281DI4o8y1usi2TPcd7ouhSmvz2oS7tIno6zLpU3CiuQrW96TjD+7RMKtiUkZRHQbMqYnJJoQqa2YSjxZQjA5JUUQOV3tcfS2pnwzgxF/AU=
\ No newline at end of file
diff --git a/tests/data/logout_requests/invalids/not_after_failed.xml b/tests/data/logout_requests/invalids/not_after_failed.xml
index 8f4e2beb..1f825036 100644
--- a/tests/data/logout_requests/invalids/not_after_failed.xml
+++ b/tests/data/logout_requests/invalids/not_after_failed.xml
@@ -9,6 +9,6 @@
>
http://idp.example.com/
ONELOGIN_1e442c129e1f822c8096086a1103c5ee2c7cae1c
diff --git a/tests/data/logout_requests/invalids/not_after_failed.xml.base64 b/tests/data/logout_requests/invalids/not_after_failed.xml.base64
index 753d4b81..8ba510d0 100644
--- a/tests/data/logout_requests/invalids/not_after_failed.xml.base64
+++ b/tests/data/logout_requests/invalids/not_after_failed.xml.base64
@@ -1 +1 @@
-jVJNT8MwDL3vV1S9r036xRptnSYNUKWxAUMcuKCQuqxSm4Q6Rfv5ZGXADutETpbz/Pxsv+l839TOJ7RYKTlzqUfceTaaIm9qzVbqXXXmET46QONYoETW/8zcrpVMcayQSd4AMiPYdnG3YoFHmG6VUULV7sg59/54LtNwRGiNlTXAky9n7mZ9vdrc5uvXgMaTSIii5IKHCadRzCFNSRQFhSjS5K2EhNCYwgDX888CbOOhdogd5BINl8biCA3HNBhT8kQiFqYspC8DhUu7vEpy0/PvjNHM99F0ZekJ1fggC60qafAkwho9vdMDfGtlNnLTLkoD7VEIif8jJOvT/W1ZP02bHeVUhfZgzxtdQy9q6p+CTsrW9kr50tneH4KHjtdVWR1UDPCcEXKj2oaby5c/ZKpiXPZQ1knUIA6NijN82a8FKNhrCxqkQMtJEIgJSRMysV6gJBQxQCCuBAcqjsN9j2K97p8xe/YF
+jVJNT8MwDL1P2n+Yel+b9Is12jpNGqBKYwOGOHBBIXVZpTYJdYr282m7ATu0iJwsx37v2X7z5bEsJp9QYa7kwqI2sZbxeDRHXhaabdS7qs0jfNSAZtJUSmTdz8KqK8kUxxyZ5CUgM4LtV3cb5tqE6UoZJVRhjUeTvvcL9DcOR4TKNMKGgJL1wtptrze722T76tJg5guRZlxwL+TUDzhEEfF9NxVpFL5lEBIaUBgCe/5eQkM9SIhYQyLRcGmaQkK9KXWnlDwRn3kR8+jLUOe62WAuuekYDsZo5jho6iyzhSodkKlWuTR4EWGBtj7oIcCtMju5q1aZgeoshQT/khKf8t2NWTdRFZ8V5am24chLXUCna+5cFl32bZtrJevJ/r4NHmpe5FneChkA6tNyo6qSm2ELUJt2mTydZl0pqyVqEC1T2gcY/3iBQnN2Qd0IaDZzXTEjUUhmjSko8UQA4IorwYGK83ynYVrfOz3Gj78A
\ No newline at end of file
diff --git a/tests/data/logout_requests/logout_request.xml b/tests/data/logout_requests/logout_request.xml
index b3b99bee..10904a28 100644
--- a/tests/data/logout_requests/logout_request.xml
+++ b/tests/data/logout_requests/logout_request.xml
@@ -8,6 +8,6 @@
>
http://idp.example.com/
ONELOGIN_1e442c129e1f822c8096086a1103c5ee2c7cae1c
diff --git a/tests/data/logout_requests/logout_request.xml.base64 b/tests/data/logout_requests/logout_request.xml.base64
index adca75e1..870a6383 100644
--- a/tests/data/logout_requests/logout_request.xml.base64
+++ b/tests/data/logout_requests/logout_request.xml.base64
@@ -1 +1 @@
-PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6TG9nb3V0UmVxdWVzdCB4bWxuczpzYW1scD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIg0KICAgICAgICAgICAgICAgICAgICAgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiINCiAgICAgICAgICAgICAgICAgICAgIElEPSJPTkVMT0dJTl8yMTU4NGNjZGZhY2EzNmExNDVhZTk5MDQ0MmRjZDk2YmZlNjAxNTFlIg0KICAgICAgICAgICAgICAgICAgICAgVmVyc2lvbj0iMi4wIg0KICAgICAgICAgICAgICAgICAgICAgSXNzdWVJbnN0YW50PSIyMDEzLTEyLTEwVDA0OjM5OjMxWiINCiAgICAgICAgICAgICAgICAgICAgIERlc3RpbmF0aW9uPSJodHRwOi8vc3R1ZmYuY29tL2VuZHBvaW50cy9lbmRwb2ludHMvc2xzLnBocCINCiAgICAgICAgICAgICAgICAgICAgID4NCiAgICA8c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPg0KICAgIDxzYW1sOk5hbWVJRCBTUE5hbWVRdWFsaWZpZXI9Imh0dHA6Ly9pZHAuZXhhbXBsZS5jb20vIg0KICAgICAgICAgICAgICAgICBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpuYW1laWQtZm9ybWF0OnVuc3BlY2lmaWVkIg0KICAgICAgICAgICAgICAgICA+T05FTE9HSU5fMWU0NDJjMTI5ZTFmODIyYzgwOTYwODZhMTEwM2M1ZWUyYzdjYWUxYzwvc2FtbDpOYW1lSUQ+DQo8L3NhbWxwOkxvZ291dFJlcXVlc3Q+
+PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6TG9nb3V0UmVxdWVzdCB4bWxuczpzYW1scD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIg0KICAgICAgICAgICAgICAgICAgICAgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiINCiAgICAgICAgICAgICAgICAgICAgIElEPSJPTkVMT0dJTl8yMTU4NGNjZGZhY2EzNmExNDVhZTk5MDQ0MmRjZDk2YmZlNjAxNTFlIg0KICAgICAgICAgICAgICAgICAgICAgVmVyc2lvbj0iMi4wIg0KICAgICAgICAgICAgICAgICAgICAgSXNzdWVJbnN0YW50PSIyMDEzLTEyLTEwVDA0OjM5OjMxWiINCiAgICAgICAgICAgICAgICAgICAgIERlc3RpbmF0aW9uPSJodHRwOi8vc3R1ZmYuY29tL2VuZHBvaW50cy9lbmRwb2ludHMvc2xzLnBocCINCiAgICAgICAgICAgICAgICAgICAgID4NCiAgICA8c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPg0KICAgIDxzYW1sOk5hbWVJRCBTUE5hbWVRdWFsaWZpZXI9Imh0dHA6Ly9pZHAuZXhhbXBsZS5jb20vIg0KICAgICAgICAgICAgICAgICBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OnVuc3BlY2lmaWVkIg0KICAgICAgICAgICAgICAgICA+T05FTE9HSU5fMWU0NDJjMTI5ZTFmODIyYzgwOTYwODZhMTEwM2M1ZWUyYzdjYWUxYzwvc2FtbDpOYW1lSUQ+DQo8L3NhbWxwOkxvZ291dFJlcXVlc3Q+
\ No newline at end of file
diff --git a/tests/data/logout_requests/logout_request_deflated.xml.base64 b/tests/data/logout_requests/logout_request_deflated.xml.base64
index 4c0a089e..894de1ec 100644
--- a/tests/data/logout_requests/logout_request_deflated.xml.base64
+++ b/tests/data/logout_requests/logout_request_deflated.xml.base64
@@ -1 +1 @@
-fZLfT8IwEMff+SvI3tnabUzWwIgJapYgqBgffDG1u0mTra27zvDnWyYiMYw+Xa53n/vxvel8V1fDL2hQajXzqE+8eTaYIq8rw5b6Q7f2CT5bQDt0gQpZ9zPz2kYxzVEiU7wGZFawzfX9koU+YabRVgtdeYPhuffHuYzhiNBY11YPJ1/MvPXqZrm+y1dvIR1PYiGKkgseJZzGYw5pSuI4LESRJu8lJISOKfSwXn4X4Ar3lUNsIVdoubIujtBoRMMRJc8kZlHKIvrak7hwy5OK246/tdawIEDblqUvdB2AKoyWyuKJhRX6Zmt6eFnn7iRiXVNNdqDKwviw47WpoGNPg9Ogk7SVW3a+GG4e9sZjyytZSmiO3f3nnGnkVjc1t5cF3HtkMSq7UNYqNCD2hYozvOyoJAUnmqBhCrSchKGYkDQhEycpJZEYA4TiSnCg4jDczyjuZIMzN5t9Aw==
+fZJNT4NAEIbvTfofCPfCLh8VNi2NSdWQ1Fat8eDFrMtgSWB3ZRbTny+ltTYG3NNkduaZj3dmi31VWl9QY6Hk3KYOsRfJeDRDXpWardSHaswTfDaAxmojJbLuZ243tWSKY4FM8gqQGcG21/cr5jmE6VoZJVRpj0dW3/sF/c/hiFCbtrEhULqc25v1zWpzl67fPBpGgRBZzgX3p5wGIYc4JkHgZSKLp+85TAkNKQzBXn6W0JYeLIjYQCrRcGnaQEL9CfUmlDyTgPkx8+nrUOay3WAhuekq7IzRzHXRNHnuCFW5IDOtCmnwwsISHb3TQ8Dk6O+UYl1fdXLiFpl2YM8rXUJHn7mXQZd563bn6dLaPhyMx4aXRV5AfW7wL6ivl1tVV9wMC0kd2nmKbJJ3oayRqEEcKmV9wOSsKIVWPEG9GGgeeZ6ISDwlUSstJb4IATxxJThQcZrvOMzhet2e802+AQ==
\ No newline at end of file
diff --git a/tests/data/logout_requests/logout_request_with_sessionindex.xml b/tests/data/logout_requests/logout_request_with_sessionindex.xml
index dd6de13a..5dbc3c32 100644
--- a/tests/data/logout_requests/logout_request_with_sessionindex.xml
+++ b/tests/data/logout_requests/logout_request_with_sessionindex.xml
@@ -8,7 +8,7 @@
>
http://idp.example.com/
ONELOGIN_1e442c129e1f822c8096086a1103c5ee2c7cae1c
_ac72a76526cb6ca19f8438e73879a0e6c8ae5131
diff --git a/tests/data/metadata/expired_metadata_settings1.xml b/tests/data/metadata/expired_metadata_settings1.xml
index 6311dd69..9af134d8 100644
--- a/tests/data/metadata/expired_metadata_settings1.xml
+++ b/tests/data/metadata/expired_metadata_settings1.xml
@@ -3,7 +3,7 @@
MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo
- urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified
+ urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified
diff --git a/tests/data/metadata/metadata_bad_order_settings1.xml b/tests/data/metadata/metadata_bad_order_settings1.xml
index 1d1ff9d2..9d100de4 100644
--- a/tests/data/metadata/metadata_bad_order_settings1.xml
+++ b/tests/data/metadata/metadata_bad_order_settings1.xml
@@ -4,7 +4,7 @@
cacheDuration="PT1594475551S"
entityID="http://stuff.com/endpoints/metadata.php">
- urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified
+ urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified
diff --git a/tests/data/metadata/metadata_settings1.xml b/tests/data/metadata/metadata_settings1.xml
index 449a8f01..c9529f00 100644
--- a/tests/data/metadata/metadata_settings1.xml
+++ b/tests/data/metadata/metadata_settings1.xml
@@ -6,7 +6,7 @@
- urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified
+ urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified
diff --git a/tests/data/metadata/no_expiration_mark_metadata.xml b/tests/data/metadata/no_expiration_mark_metadata.xml
index 0d0b8085..92c42ba8 100644
--- a/tests/data/metadata/no_expiration_mark_metadata.xml
+++ b/tests/data/metadata/no_expiration_mark_metadata.xml
@@ -4,7 +4,7 @@
- urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified
+ urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified
diff --git a/tests/data/metadata/noentity_metadata_settings1.xml b/tests/data/metadata/noentity_metadata_settings1.xml
index af1ff3eb..b773de92 100644
--- a/tests/data/metadata/noentity_metadata_settings1.xml
+++ b/tests/data/metadata/noentity_metadata_settings1.xml
@@ -3,7 +3,7 @@
MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo
- urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified
+ urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified
diff --git a/tests/data/metadata/unparsed_metadata.xml b/tests/data/metadata/unparsed_metadata.xml
index 6e6c567a..691824da 100644
--- a/tests/data/metadata/unparsed_metadata.xml
+++ b/tests/data/metadata/unparsed_metadata.xml
@@ -6,7 +6,7 @@
- urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified
+ urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified
diff --git a/tests/data/responses/response_encrypted_nameid.xml.base64 b/tests/data/responses/response_encrypted_nameid.xml.base64
index d95040d0..6b1c9856 100644
--- a/tests/data/responses/response_encrypted_nameid.xml.base64
+++ b/tests/data/responses/response_encrypted_nameid.xml.base64
@@ -1 +1 @@
-PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIElEPSJwZng4NDMwYjFkZS1mNTU0LTQ5M2YtNjMyZS00YTIxZTBhMGRkZDQiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDE0LTAzLTA5VDEyOjIzOjM3WiIgRGVzdGluYXRpb249Imh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvbmV3b25lbG9naW4vZGVtbzEvaW5kZXgucGhwP2FjcyIgSW5SZXNwb25zZVRvPSJPTkVMT0dJTl9iZjM3MmI5ZDY3ZDBjODlkMGNmMWFmM2ZmNjI1ZWE3YzA1MWM5ODg1Ij48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPgogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+CiAgICA8ZHM6U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3JzYS1zaGExIi8+CiAgPGRzOlJlZmVyZW5jZSBVUkk9IiNwZng4NDMwYjFkZS1mNTU0LTQ5M2YtNjMyZS00YTIxZTBhMGRkZDQiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPkhENDgxVEsxVmtPMGVaNGUxL2Vma1VJTVVCaz08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+bWZrcnh5SlU4MG1hMWwwQ2xUU2tObTgzajVZQ0Y3YUt2bDJqRmVsVU04M3RuaitwNDB1RXRITjkrbHBJQVgrZEZkNnNCR3JEdUk0Q21lOTZuQ2lDK05mb2Y5MnpYaUcrWExCbFR2K0Z0cWxaZUgxc0ZNTFhuOWwxTHNLT0dPbjM5alA4bklUSGtYL0VkYjRucENNZlpQeWZ2b3M1dkMwbUtnWlFKZ1JtcG5vPTwvZHM6U2lnbmF0dXJlVmFsdWU+CjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeGQwOGM5NTY3LWU3NTEtNjY4Mi0wYTBhLWE0NTBkNmYxNTFlMCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDMtMDlUMTI6MjM6MzdaIj48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPgogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+CiAgICA8ZHM6U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3JzYS1zaGExIi8+CiAgPGRzOlJlZmVyZW5jZSBVUkk9IiNwZnhkMDhjOTU2Ny1lNzUxLTY2ODItMGEwYS1hNDUwZDZmMTUxZTAiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPjZqVkYxVkQwYnBVbUFWbURHOUt4bGoyYU5RMD08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+VTdhN0VVZ3F4MWJEL1lpb25OV09VZUwwdElHR1FJTlp3VXo3UUE4M2FibGVWcTl0a0ptSy9abzBqTXQyeW5YeDF6UDgrQzVuM0NFNjhOdnhqd3lLZEVBQTFSMDBuWmd4NExHQmdlaXZZUGtrNEFvSnpDREFsTXUzVUt6VnRJT3R1NXVWMU0yZUZzQ0ljZFRxcFEwa3ZvalVBRVlZWE5tK3pjbGRydVlYZDV3PTwvZHM6U2lnbmF0dXJlVmFsdWU+CjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWw6U3ViamVjdD48c2FtbDpFbmNyeXB0ZWRJRD48eGVuYzpFbmNyeXB0ZWREYXRhIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyIgeG1sbnM6ZHNpZz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyIgVHlwZT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjRWxlbWVudCI+PHhlbmM6RW5jcnlwdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI2FlczEyOC1jYmMiLz48ZHNpZzpLZXlJbmZvIHhtbG5zOmRzaWc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPjx4ZW5jOkVuY3J5cHRlZEtleT48eGVuYzpFbmNyeXB0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjcnNhLTFfNSIvPjx4ZW5jOkNpcGhlckRhdGE+PHhlbmM6Q2lwaGVyVmFsdWU+RjJQVjV4TjVFM3EzODFJV2Qya25CdjAvUEpPN1ExR01oMGhkUm1USUxSYnJGZ0toNlFtN2VoTFlBS1B5QW52b0lFRDZSK0g4VkhweEtsZ2lybm9xNWYvSUxCajhOV1FpOEFKV21qZkMyWTBxQ3duOHJxVFRWQzVlZFhFZUMzU094ZHBTbzNzWGlOakhKUms3VndRUEV5RXNNeWcvSkN1cytGeUJYbXRickMwPTwveGVuYzpDaXBoZXJWYWx1ZT48L3hlbmM6Q2lwaGVyRGF0YT48L3hlbmM6RW5jcnlwdGVkS2V5PjwvZHNpZzpLZXlJbmZvPgogICA8eGVuYzpDaXBoZXJEYXRhPgogICAgICA8eGVuYzpDaXBoZXJWYWx1ZT5sbjMrVTFWTTJjQ0I1TjF3SHJnM2llTDM0VEg3RXQvRGVYbytBTlJHRE52OXhtRk9zdWFKbmcwdS9XbHl6RjRNdHFsaWViRGVDQjVTWVRPRURxNFRsVVJ6NkFYSHBINVEzWWthK25rbWV5YWMrVnNpcjRwenlwblJoL29MeWRRV3dNcUlmTUIwM3hrc3l4SVM1Qk5kSzRjYnpaa3Z0bGtYL0ZrRnZUQnJLUzBsU1A4M2VlV2l4STRNell2QTVtK1R1SXpjRXQrUUdvc0FQZ0pCdERITEdYaUlhenRSWGFCSzV0eUxLcDdCMnVHVytWY2JaUTVmZVdlUTVtQ0p4WFBYYjJ0OXNMQ1ExRUVBQ2dOQitJMGFiejFhS1l4TitFandQd2pDVVFaZnFnYjlVY0xQRXBDanFzckdEMFpPSWxQYjJRcmJjNHFWc2NMaUlWRDZvaWxGWFM5QzhzbTA3dmJJTzA3SUhoU1ovRm9jK3AzbCsvQVRVb0I4eTc1SEp2TzE8L3hlbmM6Q2lwaGVyVmFsdWU+CiAgIDwveGVuYzpDaXBoZXJEYXRhPgo8L3hlbmM6RW5jcnlwdGVkRGF0YT48L3NhbWw6RW5jcnlwdGVkSUQ+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAyMy0wOS0xMFQxNzo0MzozN1oiIFJlY2lwaWVudD0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOX2JmMzcyYjlkNjdkMGM4OWQwY2YxYWYzZmY2MjVlYTdjMDUxYzk4ODUiLz48L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj48L3NhbWw6U3ViamVjdD48c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxNC0wMy0wOVQxMjoyMzowN1oiIE5vdE9uT3JBZnRlcj0iMjAyMy0wOS0xMFQxNzo0MzozN1oiPjxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PHNhbWw6QXVkaWVuY2U+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9tZXRhZGF0YS5waHA8L3NhbWw6QXVkaWVuY2U+PC9zYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PC9zYW1sOkNvbmRpdGlvbnM+PHNhbWw6QXV0aG5TdGF0ZW1lbnQgQXV0aG5JbnN0YW50PSIyMDE0LTAzLTA5VDEyOjIzOjM3WiIgU2Vzc2lvbk5vdE9uT3JBZnRlcj0iMjAxNC0wMy0wOVQyMDoyMzozN1oiIFNlc3Npb25JbmRleD0iXzk0NGJmY2FjYjBkODMyYjEyZTRiY2Y3NzRlMDJiYmU1ZjY0NTVjNjgwMyI+PHNhbWw6QXV0aG5Db250ZXh0PjxzYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkPC9zYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPjwvc2FtbDpBdXRobkNvbnRleHQ+PC9zYW1sOkF1dGhuU3RhdGVtZW50PjxzYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGUgTmFtZT0idWlkIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj50ZXN0PC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9Im1haWwiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnRlc3RAZXhhbXBsZS5jb208L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0iY24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnRlc3Q8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0ic24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPndhYTI8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0iZWR1UGVyc29uQWZmaWxpYXRpb24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnVzZXI8L3NhbWw6QXR0cmlidXRlVmFsdWU+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+YWRtaW48L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48L3NhbWw6QXR0cmlidXRlU3RhdGVtZW50Pjwvc2FtbDpBc3NlcnRpb24+PC9zYW1scDpSZXNwb25zZT4=
+PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGNlNWNjMjE0LTI1OGMtNjNkYy1iM2UxLWQ4YmI3ZGQ1ZWM1ZiIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDMtMDlUMTI6MjM6MzdaIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOX2JmMzcyYjlkNjdkMGM4OWQwY2YxYWYzZmY2MjVlYTdjMDUxYzk4ODUiPjxzYW1sOklzc3Vlcj5odHRwczovL3BpdGJ1bGsubm8taXAub3JnL3NpbXBsZXNhbWwvc2FtbDIvaWRwL21ldGFkYXRhLnBocDwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+DQogICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPg0KICA8ZHM6UmVmZXJlbmNlIFVSST0iI3BmeGNlNWNjMjE0LTI1OGMtNjNkYy1iM2UxLWQ4YmI3ZGQ1ZWM1ZiI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz48ZHM6RGlnZXN0VmFsdWU+aWptQ20xSDcxUE44TENNWWprbmx2YUVLMDB3PTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT5SeDNCODBrSUJnZm1UeU43Sy9HRXFNL0VmQWgwRVllV1NJN2RxdzBzdTRQUDdKclhjcng0N2ZwSjlLYWpRWlBTOU5CcVdjZlhJeVNVdGR5c1l4SUU3VzIvOHd0ZFozakVzbzZLRWlreDBTTlA0Z0RkUzNyNzVLMVNMb3NBZklVSmg2L0lUbll1Q2Y5UFl3RHNXVSt2WU9la2xnWC9xT3lkbm5sKzQ1QmpTd2M9PC9kczpTaWduYXR1cmVWYWx1ZT4NCjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDc1NmNjMjc5LWFmMjEtZDE5Mi1mMGVkLTEyNmRjZTYwMzBkNSIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDMtMDlUMTI6MjM6MzdaIj48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPg0KICA8ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPg0KICAgIDxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjcnNhLXNoYTEiLz4NCiAgPGRzOlJlZmVyZW5jZSBVUkk9IiNwZng3NTZjYzI3OS1hZjIxLWQxOTItZjBlZC0xMjZkY2U2MDMwZDUiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPks0MkdwcnJwREpLNmUvRlhpazBZYjl0cVpIVT08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+Z1pYSVkxWEw2UitNYzBzYWE5NzdSRzZEOXYzdUNkYUtXTUF5S0pUZUQ5UHIrL0NrWFNDbjJpbERXbUQyaldEVUVGNEZnZWtBMEt6STZQamRLR3VwRmVuZW96a2pFcHVmU0ozRkhpSXNxRnM5OGdONWZvZEEzRm16RjBLS2dScTRJaVRSd216UG5xT080eE8rQlhNQkoyTkFPVENaYnRyb3RsYnVBVlFUT3lzPTwvZHM6U2lnbmF0dXJlVmFsdWU+DQo8ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE+PGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlDZ1RDQ0Flb0NDUUNiT2xyV0RkWDdGVEFOQmdrcWhraUc5dzBCQVFVRkFEQ0JoREVMTUFrR0ExVUVCaE1DVGs4eEdEQVdCZ05WQkFnVEQwRnVaSEpsWVhNZ1UyOXNZbVZ5WnpFTU1Bb0dBMVVFQnhNRFJtOXZNUkF3RGdZRFZRUUtFd2RWVGtsT1JWUlVNUmd3RmdZRFZRUURFdzltWldsa1pTNWxjbXhoYm1jdWJtOHhJVEFmQmdrcWhraUc5dzBCQ1FFV0VtRnVaSEpsWVhOQWRXNXBibVYwZEM1dWJ6QWVGdzB3TnpBMk1UVXhNakF4TXpWYUZ3MHdOekE0TVRReE1qQXhNelZhTUlHRU1Rc3dDUVlEVlFRR0V3Sk9UekVZTUJZR0ExVUVDQk1QUVc1a2NtVmhjeUJUYjJ4aVpYSm5NUXd3Q2dZRFZRUUhFd05HYjI4eEVEQU9CZ05WQkFvVEIxVk9TVTVGVkZReEdEQVdCZ05WQkFNVEQyWmxhV1JsTG1WeWJHRnVaeTV1YnpFaE1COEdDU3FHU0liM0RRRUpBUllTWVc1a2NtVmhjMEIxYm1sdVpYUjBMbTV2TUlHZk1BMEdDU3FHU0liM0RRRUJBUVVBQTRHTkFEQ0JpUUtCZ1FEaXZiaFI3UDUxNngvUzNCcUt4dXBRZTBMT05vbGl1cGlCT2VzQ08zU0hiRHJsMytxOUliZm5mbUUwNHJOdU1jUHNJeEIxNjFUZERwSWVzTENuN2M4YVBISVNLT3RQbEFlVFpTbmI4UUF1N2FSalpxMytQYnJQNXVXM1RjZkNHUHRLVHl0SE9nZS9PbEpibzA3OGRWaFhRMTRkMUVEd1hKVzFyUlh1VXQ0QzhRSURBUUFCTUEwR0NTcUdTSWIzRFFFQkJRVUFBNEdCQUNEVmZwODZIT2JxWStlOEJVb1dROStWTVF4MUFTRG9oQmp3T3NnMld5a1VxUlhGK2RMZmNVSDlkV1I2M0N0WklLRkRiU3ROb21QblF6N25iSytvbnlnd0JzcFZFYm5IdVVpaFpxM1pVZG11bVFxQ3c0VXZzLzFVdnEzb3JPby9XSlZoVHl2TGdGVksyUWFyUTQvNjdPWmZIZDdSK1BPQlhob3BoU012MVpPbzwvZHM6WDUwOUNlcnRpZmljYXRlPjwvZHM6WDUwOURhdGE+PC9kczpLZXlJbmZvPjwvZHM6U2lnbmF0dXJlPjxzYW1sOlN1YmplY3Q+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAyMy0wOS0xMFQxNzo0MzozN1oiIFJlY2lwaWVudD0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOX2JmMzcyYjlkNjdkMGM4OWQwY2YxYWYzZmY2MjVlYTdjMDUxYzk4ODUiLz48L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj48c2FtbDpFbmNyeXB0ZWRJRD48eGVuYzpFbmNyeXB0ZWREYXRhIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyIgeG1sbnM6ZHNpZz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyIgVHlwZT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjRWxlbWVudCI+PHhlbmM6RW5jcnlwdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI3RyaXBsZWRlcy1jYmMiLz48ZHNpZzpLZXlJbmZvIHhtbG5zOmRzaWc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPjx4ZW5jOkVuY3J5cHRlZEtleT48eGVuYzpFbmNyeXB0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjcnNhLTFfNSIvPjx4ZW5jOkNpcGhlckRhdGE+PHhlbmM6Q2lwaGVyVmFsdWU+Mkx5Ry9wb1RrZUFsVHAvNmpEWUhGQ0JFbU9wUmZGUE5qZ2R5WDRWYTY5RDFkOXMycUIvcHB0UUg0UE8wZjl3cjVsbm9hWUhKME9CWUJJRjdnS1lEOVcvOEpVTUhLN1lNS2svandJcnhmWDNpVlRMQ1VMaTRoSWdRVUhRckR0cXg0eFNsNTJBckcrYzRxcDhNQk5ydTZ5T2lqSzB0NkRnemJGZURlNExxb0ZjPTwveGVuYzpDaXBoZXJWYWx1ZT48L3hlbmM6Q2lwaGVyRGF0YT48L3hlbmM6RW5jcnlwdGVkS2V5PjwvZHNpZzpLZXlJbmZvPg0KICAgPHhlbmM6Q2lwaGVyRGF0YT4NCiAgICAgIDx4ZW5jOkNpcGhlclZhbHVlPkJNWHRJZENXM2N6S0h5dk1tTGswMW9MWXNPY0NRcmFQblFzT3ZWN3pKL2Zzbnc3cG1pR2V5TlBsZFJQS2VRSVk1M2hJYTR4WUR5bmpUcHNHUERXZFpDUWo4US83R0R1TlMrSnArdUY2WWZSRzd1bEIyanBhU0xBVkRBRUt3eXVUVTBrZHhnTEw0L1BTZFVTQ1B1SURUMS94UURuZ2F6Q2I5RTNtY1ZsajZSODlxano0R1A5cVBlSC95WmZoV1EwYzRpNUs5NnZWOXFud09ZTUtSWjhHdFBLemNyK2RsbFhSSHlHQ09nOGtyUjd3Y0QxR1BvaHZmdVhvdm5xR3hMKzVPZExrMjVRbDJ4Y1Q1cUxNZnhGaEVxRDNteHVIVmN3WTwveGVuYzpDaXBoZXJWYWx1ZT4NCiAgIDwveGVuYzpDaXBoZXJEYXRhPg0KPC94ZW5jOkVuY3J5cHRlZERhdGE+PC9zYW1sOkVuY3J5cHRlZElEPjwvc2FtbDpTdWJqZWN0PjxzYW1sOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDE0LTAzLTA5VDEyOjIzOjA3WiIgTm90T25PckFmdGVyPSIyMDIzLTA5LTEwVDE3OjQzOjM3WiI+PHNhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48c2FtbDpBdWRpZW5jZT5odHRwczovL3BpdGJ1bGsubm8taXAub3JnL25ld29uZWxvZ2luL2RlbW8xL21ldGFkYXRhLnBocDwvc2FtbDpBdWRpZW5jZT48L3NhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48L3NhbWw6Q29uZGl0aW9ucz48c2FtbDpBdXRoblN0YXRlbWVudCBBdXRobkluc3RhbnQ9IjIwMTQtMDMtMDlUMTI6MjM6MzdaIiBTZXNzaW9uTm90T25PckFmdGVyPSIyMDE0LTAzLTA5VDIwOjIzOjM3WiIgU2Vzc2lvbkluZGV4PSJfOTQ0YmZjYWNiMGQ4MzJiMTJlNGJjZjc3NGUwMmJiZTVmNjQ1NWM2ODAzIj48c2FtbDpBdXRobkNvbnRleHQ+PHNhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFjOmNsYXNzZXM6UGFzc3dvcmQ8L3NhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+PC9zYW1sOkF1dGhuQ29udGV4dD48L3NhbWw6QXV0aG5TdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlU3RhdGVtZW50PjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJ1aWQiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnRlc3Q8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0ibWFpbCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dGVzdEBleGFtcGxlLmNvbTwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJjbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dGVzdDwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJzbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+d2FhMjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJlZHVQZXJzb25BZmZpbGlhdGlvbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dXNlcjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5hZG1pbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PC9zYW1sOkFzc2VydGlvbj48L3NhbWxwOlJlc3BvbnNlPg==
diff --git a/tests/settings/settings1.json b/tests/settings/settings1.json
index d809b7d6..69d7d25e 100644
--- a/tests/settings/settings1.json
+++ b/tests/settings/settings1.json
@@ -10,7 +10,7 @@
"singleLogoutService": {
"url": "http://stuff.com/endpoints/endpoints/sls.php"
},
- "NameIDFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified"
+ "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
},
"idp": {
"entityId": "http://idp.example.com/",
diff --git a/tests/settings/settings2.json b/tests/settings/settings2.json
index d18b518f..22f92dc1 100644
--- a/tests/settings/settings2.json
+++ b/tests/settings/settings2.json
@@ -10,7 +10,7 @@
"singleLogoutService": {
"url": "http://stuff.com/endpoints/endpoints/sls.php"
},
- "NameIDFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified"
+ "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
},
"idp": {
"entityId": "https://idp.example.com/simplesaml/saml2/idp/metadata.php",
diff --git a/tests/settings/settings3.json b/tests/settings/settings3.json
index c291c615..de72e50d 100644
--- a/tests/settings/settings3.json
+++ b/tests/settings/settings3.json
@@ -10,7 +10,7 @@
"singleLogoutService": {
"url": "http://pytoolkit.com:8000/?sls"
},
- "NameIDFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified"
+ "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
},
"idp": {
"entityId": "https://pitbulk.no-ip.org/simplesaml/saml2/idp/metadata.php",
diff --git a/tests/settings/settings4.json b/tests/settings/settings4.json
index 1300c7df..c217c7d8 100644
--- a/tests/settings/settings4.json
+++ b/tests/settings/settings4.json
@@ -46,7 +46,7 @@
"singleLogoutService": {
"url": "http://pytoolkit.com:8000/?sls"
},
- "NameIDFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified"
+ "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
},
"idp": {
"entityId": "https://pitbulk.no-ip.org/simplesaml/saml2/idp/metadata.php",
diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py
index 1bc0a577..2d296e3c 100644
--- a/tests/src/OneLogin/saml2_tests/auth_test.py
+++ b/tests/src/OneLogin/saml2_tests/auth_test.py
@@ -864,7 +864,7 @@ def testBuildRequestSignature(self):
auth = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings)
signature = auth.build_request_signature(message, relay_state)
- valid_signature = 'E17GU1STzanOXxBTKjweB1DovP8aMJdj5BEy0fnGoEslKdP6hpPc3enjT/bu7I8D8QzLoir8SxZVWdUDXgIxJIEgfK5snr+jJwfc5U2HujsOa/Xb3c4swoyPcyQhcxLRDhDjPq5cQxJfYoPeElvCuI6HAD1mtdd5PS/xDvbIxuw='
+ valid_signature = 'Pb1EXAX5TyipSJ1SndEKZstLQTsT+1D00IZAhEepBM+OkAZQSToivu3njgJu47HZiZAqgXZFgloBuuWE/+GdcSsRYEMkEkiSDWTpUr25zKYLJDSg6GNo6iAHsKSuFt46Z54Xe/keYxYP03Hdy97EwuuSjBzzgRc5tmpV+KC7+a0='
self.assertEqual(signature, valid_signature)
settings['sp']['privatekey'] = ''
diff --git a/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py b/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py
index 18beb65b..a9a46908 100644
--- a/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py
+++ b/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py
@@ -393,7 +393,7 @@ def testMergeSettings(self):
"url": "http://stuff.com/endpoints/endpoints/acs.php"
},
"entityId": "http://stuff.com/endpoints/metadata.php",
- "NameIDFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified"
+ "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
},
"custom_base_path": "../../../tests/data/customPath/",
"organization": {
diff --git a/tests/src/OneLogin/saml2_tests/logout_request_test.py b/tests/src/OneLogin/saml2_tests/logout_request_test.py
index 3262aaeb..34d8ea95 100644
--- a/tests/src/OneLogin/saml2_tests/logout_request_test.py
+++ b/tests/src/OneLogin/saml2_tests/logout_request_test.py
@@ -100,7 +100,7 @@ def testGetNameIdData(self):
"""
expected_name_id_data = {
'Value': 'ONELOGIN_1e442c129e1f822c8096086a1103c5ee2c7cae1c',
- 'Format': 'urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified',
+ 'Format': 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified',
'SPNameQualifier': 'http://idp.example.com/'
}
diff --git a/tests/src/OneLogin/saml2_tests/metadata_test.py b/tests/src/OneLogin/saml2_tests/metadata_test.py
index 885dd121..a3dd4386 100644
--- a/tests/src/OneLogin/saml2_tests/metadata_test.py
+++ b/tests/src/OneLogin/saml2_tests/metadata_test.py
@@ -63,7 +63,7 @@ def testBuilder(self):
self.assertIn('urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified', metadata)
+ self.assertIn('urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified ', metadata)
self.assertIn('sp_test ', metadata)
self.assertIn('', metadata)
@@ -196,7 +196,7 @@ def testSignMetadata(self):
self.assertIn(' ', signed_metadata)
- self.assertIn('urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified ', signed_metadata)
+ self.assertIn('urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified ', signed_metadata)
self.assertIn(' ', signed_metadata)
self.assertIn(' ', signed_metadata)
diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py
index 34bea8d6..435142a8 100644
--- a/tests/src/OneLogin/saml2_tests/response_test.py
+++ b/tests/src/OneLogin/saml2_tests/response_test.py
@@ -134,7 +134,7 @@ def testGetNameIdData(self):
response_2 = OneLogin_Saml2_Response(settings, xml_2)
expected_nameid_data_2 = {
'Value': '2de11defd199f8d5bb63f9b7deb265ba5c675c10',
- 'Format': 'urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified',
+ 'Format': 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified',
'SPNameQualifier': 'https://pitbulk.no-ip.org/newonelogin/demo1/metadata.php'
}
nameid_data_2 = response_2.get_nameid_data()
diff --git a/tests/src/OneLogin/saml2_tests/settings_test.py b/tests/src/OneLogin/saml2_tests/settings_test.py
index 398e09a1..c26ee910 100644
--- a/tests/src/OneLogin/saml2_tests/settings_test.py
+++ b/tests/src/OneLogin/saml2_tests/settings_test.py
@@ -395,7 +395,7 @@ def testGetSPMetadata(self):
self.assertIn('WantAssertionsSigned="false"', metadata)
self.assertIn(' ', metadata)
self.assertIn(' ', metadata)
- self.assertIn('urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified ', metadata)
+ self.assertIn('urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified ', metadata)
def testGetSPMetadataSigned(self):
"""
@@ -450,7 +450,7 @@ def generateAndCheckMetadata(self, settings):
self.assertIn('WantAssertionsSigned="false"', metadata)
self.assertIn(' ', metadata)
self.assertIn(' ', metadata)
- self.assertIn('urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified ', metadata)
+ self.assertIn('urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified ', metadata)
self.assertIn(' ', metadata)
self.assertIn(' ', metadata)
self.assertIn('
Date: Sat, 14 May 2016 19:57:27 +0200
Subject: [PATCH 015/255] Release 2.1.7
---
changelog.md | 15 +++++++++++++++
setup.py | 2 +-
2 files changed, 16 insertions(+), 1 deletion(-)
diff --git a/changelog.md b/changelog.md
index 45411dcb..695d5e38 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,5 +1,20 @@
# python-saml changelog
+### 2.1.7 (May 14, 2016)
+* [#117](https://github.com/onelogin/python-saml/pull/117) AttributeConsumingService support
+* [#114](https://github.com/onelogin/python-saml/pull/114) Compare Assertion InResponseTo if not None
+* Return empty list when there are no audience values
+* Passing NameQualifier through to logout request
+* Make deflate process when retrieving built SAML messages optional
+* Add debug parameter to decrypt method
+* Fix Idp Metadata parser
+* Add documentation related to the new IdP metadata parser methods
+* Extract the already encoded value directly from get_data
+* [#133](https://github.com/onelogin/python-saml/pull/133) Fix typo and add extra assertions in util decrypt test
+* Fix Signature with empty URI support
+* Allow AuthnRequest with no NameIDPolicy
+* Remove requirement of NameID on SAML responses
+
### 2.1.6 (Feb 15, 2016)
* Prevent signature wrapping attack!!
* [#111](https://github.com/onelogin/python-saml/pull/111) Add support for nested `NameID` children inside `AttributeValue`s
diff --git a/setup.py b/setup.py
index d14890ad..0499d50a 100644
--- a/setup.py
+++ b/setup.py
@@ -9,7 +9,7 @@
setup(
name='python-saml',
- version='2.1.6',
+ version='2.1.7',
description='Onelogin Python Toolkit. Add SAML support to your Python software using this library',
classifiers=[
'Development Status :: 4 - Beta',
From c2b087dcfaf6c771168e678f04a4f4d98ab5b1e4 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Tue, 17 May 2016 13:46:06 +0200
Subject: [PATCH 016/255] Fix #135, #136. Metadata XML (RequestedAttribute)
---
src/onelogin/saml2/metadata.py | 4 ++--
tests/src/OneLogin/saml2_tests/metadata_test.py | 10 +++++-----
2 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/src/onelogin/saml2/metadata.py b/src/onelogin/saml2/metadata.py
index 46e8415f..018ab36a 100644
--- a/src/onelogin/saml2/metadata.py
+++ b/src/onelogin/saml2/metadata.py
@@ -87,14 +87,14 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N
requested_attribute_data = []
for req_attribs in sp['attributeConsumingService']['requestedAttributes']:
req_attr_nameformat_str = req_attr_friendlyname_str = req_attr_isrequired_str = ''
- req_attr_aux_str = ' \>'
+ req_attr_aux_str = ' />'
if 'nameFormat' in req_attribs.keys() and req_attribs['nameFormat']:
req_attr_nameformat_str = " NameFormat=\"%s\"" % req_attribs['nameFormat']
if 'friendlyName' in req_attribs.keys() and req_attribs['friendlyName']:
req_attr_nameformat_str = " FriendlyName=\"%s\"" % req_attribs['friendlyName']
if 'isRequired' in req_attribs.keys() and req_attribs['isRequired']:
- req_attr_isrequired_str = " isRequired=\"%s\"" % req_attribs['isRequired']
+ req_attr_isrequired_str = " isRequired=\"%s\"" % 'true' if req_attribs['isRequired'] else 'false'
if 'attributeValue' in req_attribs.keys() and req_attribs['attributeValue']:
req_attr_aux_str = """ >
Test Service
Test Service
-
-
-
-
-
+
+
+
+
+
""", metadata)
def testSignMetadata(self):
From eade83963be2f524de068683d71e63068ff5b847 Mon Sep 17 00:00:00 2001
From: Avner Cohen
Date: Wed, 18 May 2016 16:42:12 +0300
Subject: [PATCH 017/255] Docs for OSx instlltion of libsecxml1
---
README.md | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/README.md b/README.md
index ba73879c..47af42d2 100644
--- a/README.md
+++ b/README.md
@@ -78,8 +78,19 @@ Installation
* [isodate](https://pypi.python.org/pypi/isodate) An ISO 8601 date/time/duration parser and formater
* [defusedxml](https://pypi.python.org/pypi/defusedxml) XML bomb protection for Python stdlib modules
+
Review the setup.py file to know the version of the library that python-saml is using
+### OSX Dependences ###
+ * python 2.7
+ * libxmlsec1
+
+```sh
+ # using brew
+ brew install libxmlsec1
+```
+
+
### Code ###
#### Option 1. Download from github ####
From 18014888bb464d061b5e324571b3b2aae9a5dd92 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Fri, 20 May 2016 12:44:32 +0200
Subject: [PATCH 018/255] Fix SHA384 Constant URI
---
src/onelogin/saml2/constants.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/onelogin/saml2/constants.py b/src/onelogin/saml2/constants.py
index 19916cdd..2cd2e340 100644
--- a/src/onelogin/saml2/constants.py
+++ b/src/onelogin/saml2/constants.py
@@ -91,7 +91,7 @@ class OneLogin_Saml2_Constants(object):
# Sign & Crypto
SHA1 = 'http://www.w3.org/2000/09/xmldsig#sha1'
SHA256 = 'http://www.w3.org/2001/04/xmlenc#sha256'
- SHA384 = 'http://www.w3.org/2001/04/xmlencsha384'
+ SHA384 = 'http://www.w3.org/2001/04/xmldsig-more#sha384'
SHA512 = 'http://www.w3.org/2001/04/xmlenc#sha512'
DSA_SHA1 = 'http://www.w3.org/2000/09/xmld/sig#dsa-sha1'
From ed0463e39a02868e01ce0d97b5da2e905c772fef Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Fri, 20 May 2016 18:18:34 +0200
Subject: [PATCH 019/255] Fixes Windows specific Unix date formatting bug. Use
time() instead of datetime.now().strftime('%s')
---
src/onelogin/saml2/metadata.py | 4 ++--
src/onelogin/saml2/settings.py | 4 ++--
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/onelogin/saml2/metadata.py b/src/onelogin/saml2/metadata.py
index 018ab36a..c8c40b95 100644
--- a/src/onelogin/saml2/metadata.py
+++ b/src/onelogin/saml2/metadata.py
@@ -9,7 +9,7 @@
"""
-from time import gmtime, strftime
+from time import gmtime, strftime, time
from datetime import datetime
from defusedxml.minidom import parseString
@@ -54,7 +54,7 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N
:type organization: dict
"""
if valid_until is None:
- valid_until = int(datetime.now().strftime("%s")) + OneLogin_Saml2_Metadata.TIME_VALID
+ valid_until = int(time()) + OneLogin_Saml2_Metadata.TIME_VALID
if not isinstance(valid_until, basestring):
if isinstance(valid_until, datetime):
valid_until_time = valid_until.timetuple()
diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py
index 60d8cdb5..10c6dca9 100644
--- a/src/onelogin/saml2/settings.py
+++ b/src/onelogin/saml2/settings.py
@@ -9,9 +9,9 @@
"""
-from datetime import datetime
import json
import re
+from time import time
from os.path import dirname, exists, join, sep, abspath
from xml.dom.minidom import Document
@@ -735,7 +735,7 @@ def validate_metadata(self, xml):
cache_duration = element.getAttribute('cacheDuration')
expire_time = OneLogin_Saml2_Utils.get_expire_time(cache_duration, valid_until)
- if expire_time is not None and int(datetime.now().strftime('%s')) > int(expire_time):
+ if expire_time is not None and int(time()) > int(expire_time):
errors.append('expired_xml')
# TODO: Validate Sign
From 70988bdb08228da8d085e5b5498e138a716b7823 Mon Sep 17 00:00:00 2001
From: Sebastian Brachi
Date: Tue, 24 May 2016 15:13:01 -0300
Subject: [PATCH 020/255] Refactor of settings.py to make it a little more
readable. No functionality changed
---
src/onelogin/saml2/settings.py | 194 ++++++++++++---------------------
1 file changed, 68 insertions(+), 126 deletions(-)
diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py
index 10c6dca9..315d65f0 100644
--- a/src/onelogin/saml2/settings.py
+++ b/src/onelogin/saml2/settings.py
@@ -197,20 +197,12 @@ def __load_settings_from_dict(self, settings):
self.__errors = []
self.__sp = settings['sp']
- if 'idp' in settings:
- self.__idp = settings['idp']
- if 'strict' in settings:
- self.__strict = settings['strict']
- if 'debug' in settings:
- self.__debug = settings['debug']
- if 'security' in settings:
- self.__security = settings['security']
- else:
- self.__security = {}
- if 'contactPerson' in settings:
- self.__contacts = settings['contactPerson']
- if 'organization' in settings:
- self.__organization = settings['organization']
+ self.__idp = settings.get('idp', {})
+ self.__strict = settings.get('strict', False)
+ self.__debug = settings.get('debug', False)
+ self.__security = settings.get('security', {})
+ self.__contacts = settings.get('contactPerson', {})
+ self.__organization = settings.get('organization', {})
self.__add_default_values()
return True
@@ -252,79 +244,53 @@ def __add_default_values(self):
"""
Add default values if the settings info is not complete
"""
- if 'assertionConsumerService' not in self.__sp.keys():
- self.__sp['assertionConsumerService'] = {}
- if 'binding' not in self.__sp['assertionConsumerService'].keys():
- self.__sp['assertionConsumerService']['binding'] = OneLogin_Saml2_Constants.BINDING_HTTP_POST
+ self.__sp.setdefault('assertionConsumerService', {})
+ self.__sp['assertionConsumerService'].setdefault('binding', OneLogin_Saml2_Constants.BINDING_HTTP_POST)
- if 'attributeConsumingService' not in self.__sp.keys():
- self.__sp['attributeConsumingService'] = {}
+ self.__sp.setdefault('attributeConsumingService', {})
- if 'singleLogoutService' not in self.__sp.keys():
- self.__sp['singleLogoutService'] = {}
- if 'binding' not in self.__sp['singleLogoutService']:
- self.__sp['singleLogoutService']['binding'] = OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT
+ self.__sp.setdefault('singleLogoutService', {})
+ self.__sp['singleLogoutService'].setdefault('binding', OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT)
# Related to nameID
- if 'NameIDFormat' not in self.__sp:
- self.__sp['NameIDFormat'] = OneLogin_Saml2_Constants.NAMEID_UNSPECIFIED
- if 'nameIdEncrypted' not in self.__security:
- self.__security['nameIdEncrypted'] = False
+ self.__sp.setdefault('NameIDFormat', OneLogin_Saml2_Constants.NAMEID_UNSPECIFIED)
+ self.__security.setdefault('nameIdEncrypted', False)
# Metadata format
- if 'metadataValidUntil' not in self.__security.keys():
- self.__security['metadataValidUntil'] = None # None means use default
- if 'metadataCacheDuration' not in self.__security.keys():
- self.__security['metadataCacheDuration'] = None # None means use default
+ self.__security.setdefault('metadataValidUntil', None) # None means use default
+ self.__security.setdefault('metadataCacheDuration', None) # None means use default
# Sign provided
- if 'authnRequestsSigned' not in self.__security.keys():
- self.__security['authnRequestsSigned'] = False
- if 'logoutRequestSigned' not in self.__security.keys():
- self.__security['logoutRequestSigned'] = False
- if 'logoutResponseSigned' not in self.__security.keys():
- self.__security['logoutResponseSigned'] = False
- if 'signMetadata' not in self.__security.keys():
- self.__security['signMetadata'] = False
+ self.__security.setdefault('authnRequestsSigned', False)
+ self.__security.setdefault('logoutRequestSigned', False)
+ self.__security.setdefault('logoutResponseSigned', False)
+ self.__security.setdefault('signMetadata', False)
# Sign expected
- if 'wantMessagesSigned' not in self.__security.keys():
- self.__security['wantMessagesSigned'] = False
- if 'wantAssertionsSigned' not in self.__security.keys():
- self.__security['wantAssertionsSigned'] = False
+ self.__security.setdefault('wantMessagesSigned', False)
+ self.__security.setdefault('wantAssertionsSigned', False)
# NameID element expected
- if 'wantNameId' not in self.__security.keys():
- self.__security['wantNameId'] = True
+ self.__security.setdefault('wantNameId', True)
# Encrypt expected
- if 'wantAssertionsEncrypted' not in self.__security.keys():
- self.__security['wantAssertionsEncrypted'] = False
- if 'wantNameIdEncrypted' not in self.__security.keys():
- self.__security['wantNameIdEncrypted'] = False
+ self.__security.setdefault('wantAssertionsEncrypted', False)
+ self.__security.setdefault('wantNameIdEncrypted', False)
# Signature Algorithm
- if 'signatureAlgorithm' not in self.__security.keys():
- self.__security['signatureAlgorithm'] = OneLogin_Saml2_Constants.RSA_SHA1
+ self.__security.setdefault('signatureAlgorithm', OneLogin_Saml2_Constants.RSA_SHA1)
# AttributeStatement required by default
- if 'wantAttributeStatement' not in self.__security.keys():
- self.__security['wantAttributeStatement'] = True
+ self.__security.setdefault('wantAttributeStatement', True)
- if 'x509cert' not in self.__idp:
- self.__idp['x509cert'] = ''
- if 'certFingerprint' not in self.__idp:
- self.__idp['certFingerprint'] = ''
- if 'certFingerprintAlgorithm' not in self.__idp:
- self.__idp['certFingerprintAlgorithm'] = 'sha1'
+ self.__idp.setdefault('x509cert', '')
+ self.__idp.setdefault('certFingerprint', '')
+ self.__idp.setdefault('certFingerprintAlgorithm', 'sha1')
- if 'x509cert' not in self.__sp:
- self.__sp['x509cert'] = ''
- if 'privateKey' not in self.__sp:
- self.__sp['privateKey'] = ''
+ self.__sp.setdefault('x509cert', '')
+ self.__sp.setdefault('privateKey', '')
- if 'requestedAuthnContext' not in self.__security.keys():
- self.__security['requestedAuthnContext'] = True
+ self.__security.setdefault('requestedAuthnContext', True)
def check_settings(self, settings):
"""
@@ -365,37 +331,31 @@ def check_idp_settings(self, settings):
if not isinstance(settings, dict) or len(settings) == 0:
errors.append('invalid_syntax')
else:
- if 'idp' not in settings or len(settings['idp']) == 0:
+ if not settings.get('idp'):
errors.append('idp_not_found')
else:
idp = settings['idp']
- if 'entityId' not in idp or len(idp['entityId']) == 0:
+ if not idp.get('entityId'):
errors.append('idp_entityId_not_found')
- if 'singleSignOnService' not in idp or \
- 'url' not in idp['singleSignOnService'] or \
- len(idp['singleSignOnService']['url']) == 0:
+ if not idp.get('singleSignOnService', {}).get('url'):
errors.append('idp_sso_not_found')
elif not validate_url(idp['singleSignOnService']['url']):
errors.append('idp_sso_url_invalid')
- if 'singleLogoutService' in idp and \
- 'url' in idp['singleLogoutService'] and \
- len(idp['singleLogoutService']['url']) > 0 and \
- not validate_url(idp['singleLogoutService']['url']):
+ slo_url = idp.get('singleLogoutService', {}).get('url')
+ if slo_url and not validate_url(slo_url):
errors.append('idp_slo_url_invalid')
if 'security' in settings:
security = settings['security']
- exists_x509 = ('x509cert' in idp and
- len(idp['x509cert']) > 0)
- exists_fingerprint = ('certFingerprint' in idp and
- len(idp['certFingerprint']) > 0)
+ exists_x509 = bool(idp.get('x509cert'))
+ exists_fingerprint = bool(idp.get('certFingerprint'))
- want_assert_sign = 'wantAssertionsSigned' in security.keys() and security['wantAssertionsSigned']
- want_mes_signed = 'wantMessagesSigned' in security.keys() and security['wantMessagesSigned']
- nameid_enc = 'nameIdEncrypted' in security.keys() and security['nameIdEncrypted']
+ want_assert_sign = bool(security.get('wantAssertionsSigned'))
+ want_mes_signed = bool(security.get('wantMessagesSigned'))
+ nameid_enc = bool(security.get('nameIdEncrypted'))
if (want_assert_sign or want_mes_signed) and \
not(exists_x509 or exists_fingerprint):
@@ -418,10 +378,10 @@ def check_sp_settings(self, settings):
assert isinstance(settings, dict)
errors = []
- if not isinstance(settings, dict) or len(settings) == 0:
+ if not isinstance(settings, dict) or not settings:
errors.append('invalid_syntax')
else:
- if 'sp' not in settings or len(settings['sp']) == 0:
+ if not settings.get('sp'):
errors.append('sp_not_found')
else:
# check_sp_certs uses self.__sp so I add it
@@ -429,21 +389,17 @@ def check_sp_settings(self, settings):
self.__sp = settings['sp']
sp = settings['sp']
- security = {}
- if 'security' in settings:
- security = settings['security']
+ security = settings.get('security', {})
- if 'entityId' not in sp or len(sp['entityId']) == 0:
+ if not sp.get('entityId'):
errors.append('sp_entityId_not_found')
- if 'assertionConsumerService' not in sp or \
- 'url' not in sp['assertionConsumerService'] or \
- len(sp['assertionConsumerService']['url']) == 0:
+ if not sp.get('assertionConsumerService', {}).get('url'):
errors.append('sp_acs_not_found')
elif not validate_url(sp['assertionConsumerService']['url']):
errors.append('sp_acs_url_invalid')
- if 'attributeConsumingService' in sp and len(sp['attributeConsumingService']):
+ if sp.get('attributeConsumingService'):
attributeConsumingService = sp['attributeConsumingService']
if 'serviceName' not in attributeConsumingService:
errors.append('sp_attributeConsumingService_serviceName_not_found')
@@ -468,10 +424,8 @@ def check_sp_settings(self, settings):
if "serviceDescription" in attributeConsumingService and not isinstance(attributeConsumingService['serviceDescription'], basestring):
errors.append('sp_attributeConsumingService_serviceDescription_type_invalid')
- if 'singleLogoutService' in sp and \
- 'url' in sp['singleLogoutService'] and \
- len(sp['singleLogoutService']['url']) > 0 and \
- not validate_url(sp['singleLogoutService']['url']):
+ slo_url = sp.get('singleLogoutService', {}).get('url')
+ if slo_url and not validate_url(slo_url):
errors.append('sp_sls_url_invalid')
if 'signMetadata' in security and isinstance(security['signMetadata'], dict):
@@ -479,11 +433,11 @@ def check_sp_settings(self, settings):
'certFileName' not in security['signMetadata']:
errors.append('sp_signMetadata_invalid')
- authn_sign = 'authnRequestsSigned' in security.keys() and security['authnRequestsSigned']
- logout_req_sign = 'logoutRequestSigned' in security.keys() and security['logoutRequestSigned']
- logout_res_sign = 'logoutResponseSigned' in security.keys() and security['logoutResponseSigned']
- want_assert_enc = 'wantAssertionsEncrypted' in security.keys() and security['wantAssertionsEncrypted']
- want_nameid_enc = 'wantNameIdEncrypted' in security.keys() and security['wantNameIdEncrypted']
+ authn_sign = bool(security.get('authnRequestsSigned'))
+ logout_req_sign = bool(security.get('logoutRequestSigned'))
+ logout_res_sign = bool(security.get('logoutResponseSigned'))
+ want_assert_enc = bool(security.get('wantAssertionsEncrypted'))
+ want_nameid_enc = bool(security.get('wantNameIdEncrypted'))
if not self.check_sp_certs():
if authn_sign or logout_req_sign or logout_res_sign or \
@@ -537,18 +491,14 @@ def get_sp_key(self):
:returns: SP private key
:rtype: string
"""
- key = None
+ key = self.__sp.get('privateKey')
+ key_file_name = self.__paths['cert'] + 'sp.key'
- if 'privateKey' in self.__sp.keys() and self.__sp['privateKey']:
- key = self.__sp['privateKey']
- else:
- key_file_name = self.__paths['cert'] + 'sp.key'
+ if not key and exists(key_file_name):
+ with open(key_file_name) as f:
+ key = f.read()
- if exists(key_file_name):
- f_key = open(key_file_name, 'r')
- key = f_key.read()
- f_key.close()
- return key
+ return key or None # TODO: Should change rtype docstrings or update tests
def get_sp_cert(self):
"""
@@ -557,18 +507,14 @@ def get_sp_cert(self):
:returns: SP public cert
:rtype: string
"""
- cert = None
+ cert = self.__sp.get('x509cert')
+ cert_file_name = self.__paths['cert'] + 'sp.crt'
- if 'x509cert' in self.__sp.keys() and self.__sp['x509cert']:
- cert = self.__sp['x509cert']
- else:
- cert_file_name = self.__paths['cert'] + 'sp.crt'
- if exists(cert_file_name):
- f_cert = open(cert_file_name, 'r')
- cert = f_cert.read()
- f_cert.close()
+ if not cert and exists(cert_file_name):
+ with open(cert_file_name) as f:
+ cert = f.read()
- return cert
+ return cert or None # TODO: Should change rtype docstrings or update tests
def get_idp_cert(self):
"""
@@ -577,11 +523,7 @@ def get_idp_cert(self):
:returns: IdP public cert
:rtype: string
"""
- cert = None
-
- if 'x509cert' in self.__idp.keys() and self.__idp['x509cert']:
- cert = self.__idp['x509cert']
- return cert
+ return self.__idp.get('x509cert')
def get_idp_data(self):
"""
From 6207906ae50bb88238ff142e656ddd2f027cf3bd Mon Sep 17 00:00:00 2001
From: Sebastian Brachi
Date: Tue, 24 May 2016 19:28:18 -0300
Subject: [PATCH 021/255] Removed TODO updating docstrings
---
src/onelogin/saml2/settings.py | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py
index 315d65f0..4470cc91 100644
--- a/src/onelogin/saml2/settings.py
+++ b/src/onelogin/saml2/settings.py
@@ -489,7 +489,7 @@ def get_sp_key(self):
Returns the x509 private key of the SP.
:returns: SP private key
- :rtype: string
+ :rtype: string or None
"""
key = self.__sp.get('privateKey')
key_file_name = self.__paths['cert'] + 'sp.key'
@@ -498,14 +498,14 @@ def get_sp_key(self):
with open(key_file_name) as f:
key = f.read()
- return key or None # TODO: Should change rtype docstrings or update tests
+ return key or None
def get_sp_cert(self):
"""
Returns the x509 public cert of the SP.
:returns: SP public cert
- :rtype: string
+ :rtype: string or None
"""
cert = self.__sp.get('x509cert')
cert_file_name = self.__paths['cert'] + 'sp.crt'
@@ -514,7 +514,7 @@ def get_sp_cert(self):
with open(cert_file_name) as f:
cert = f.read()
- return cert or None # TODO: Should change rtype docstrings or update tests
+ return cert or None
def get_idp_cert(self):
"""
From 25d0bc528d1a3d3bdbf9e32af1b91e14d9823cfa Mon Sep 17 00:00:00 2001
From: Max Leblanc
Date: Wed, 25 May 2016 15:14:27 -0400
Subject: [PATCH 022/255] Bugfix for ADFS lowercase signatures
---
src/onelogin/saml2/auth.py | 6 +++---
src/onelogin/saml2/logout_request.py | 8 ++++----
src/onelogin/saml2/logout_response.py | 8 ++++----
src/onelogin/saml2/utils.py | 11 ++++++++---
4 files changed, 19 insertions(+), 14 deletions(-)
diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py
index ff06da50..5626e8ba 100644
--- a/src/onelogin/saml2/auth.py
+++ b/src/onelogin/saml2/auth.py
@@ -111,7 +111,7 @@ def process_response(self, request_id=None):
OneLogin_Saml2_Error.SAML_RESPONSE_NOT_FOUND
)
- def process_slo(self, keep_local_session=False, request_id=None, delete_session_cb=None):
+ def process_slo(self, keep_local_session=False, request_id=None, delete_session_cb=None, lowercase_urlencoding=False):
"""
Process the SAML Logout Response / Logout Request sent by the IdP.
@@ -127,7 +127,7 @@ def process_slo(self, keep_local_session=False, request_id=None, delete_session_
if 'get_data' in self.__request_data and 'SAMLResponse' in self.__request_data['get_data']:
logout_response = OneLogin_Saml2_Logout_Response(self.__settings, self.__request_data['get_data']['SAMLResponse'])
- if not logout_response.is_valid(self.__request_data, request_id):
+ if not logout_response.is_valid(self.__request_data, request_id, lowercase_urlencoding=lowercase_urlencoding):
self.__errors.append('invalid_logout_response')
self.__error_reason = logout_response.get_error()
elif logout_response.get_status() != OneLogin_Saml2_Constants.STATUS_SUCCESS:
@@ -137,7 +137,7 @@ def process_slo(self, keep_local_session=False, request_id=None, delete_session_
elif 'get_data' in self.__request_data and 'SAMLRequest' in self.__request_data['get_data']:
logout_request = OneLogin_Saml2_Logout_Request(self.__settings, self.__request_data['get_data']['SAMLRequest'])
- if not logout_request.is_valid(self.__request_data):
+ if not logout_request.is_valid(self.__request_data, lowercase_urlencoding=lowercase_urlencoding):
self.__errors.append('invalid_logout_request')
self.__error_reason = logout_request.get_error()
else:
diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py
index 8c1a83d2..96702b74 100644
--- a/src/onelogin/saml2/logout_request.py
+++ b/src/onelogin/saml2/logout_request.py
@@ -251,7 +251,7 @@ def get_session_indexes(request):
session_indexes.append(session_index_node.text)
return session_indexes
- def is_valid(self, request_data):
+ def is_valid(self, request_data, lowercase_urlencoding=False):
"""
Checks if the Logout Request recieved is valid
:param request_data: Request Data
@@ -316,10 +316,10 @@ def is_valid(self, request_data):
else:
sign_alg = get_data['SigAlg']
- signed_query = 'SAMLRequest=%s' % OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SAMLRequest')
+ signed_query = 'SAMLRequest=%s' % OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SAMLRequest', lowercase_urlencoding=lowercase_urlencoding)
if 'RelayState' in get_data:
- signed_query = '%s&RelayState=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'RelayState'))
- signed_query = '%s&SigAlg=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SigAlg', OneLogin_Saml2_Constants.RSA_SHA1))
+ signed_query = '%s&RelayState=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'RelayState', lowercase_urlencoding=lowercase_urlencoding))
+ signed_query = '%s&SigAlg=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SigAlg', OneLogin_Saml2_Constants.RSA_SHA1, lowercase_urlencoding=lowercase_urlencoding))
if 'x509cert' not in idp_data or idp_data['x509cert'] is None:
raise Exception('In order to validate the sign on the Logout Request, the x509cert of the IdP is required')
diff --git a/src/onelogin/saml2/logout_response.py b/src/onelogin/saml2/logout_response.py
index a9cce855..7a48aac6 100644
--- a/src/onelogin/saml2/logout_response.py
+++ b/src/onelogin/saml2/logout_response.py
@@ -68,7 +68,7 @@ def get_status(self):
status = entries[0].attrib['Value']
return status
- def is_valid(self, request_data, request_id=None):
+ def is_valid(self, request_data, request_id=None, lowercase_urlencoding=False):
"""
Determines if the SAML LogoutResponse is valid
:param request_id: The ID of the LogoutRequest sent by this SP to the IdP
@@ -119,10 +119,10 @@ def is_valid(self, request_data, request_id=None):
else:
sign_alg = get_data['SigAlg']
- signed_query = 'SAMLResponse=%s' % OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SAMLResponse')
+ signed_query = 'SAMLResponse=%s' % OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SAMLResponse', lowercase_urlencoding=lowercase_urlencoding)
if 'RelayState' in get_data:
- signed_query = '%s&RelayState=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'RelayState'))
- signed_query = '%s&SigAlg=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SigAlg', OneLogin_Saml2_Constants.RSA_SHA1))
+ signed_query = '%s&RelayState=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'RelayState', lowercase_urlencoding=lowercase_urlencoding))
+ signed_query = '%s&SigAlg=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SigAlg', OneLogin_Saml2_Constants.RSA_SHA1, lowercase_urlencoding=lowercase_urlencoding))
if 'x509cert' not in idp_data or idp_data['x509cert'] is None:
raise Exception('In order to validate the sign on the Logout Response, the x509cert of the IdP is required')
diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py
index eccbc5bd..24d2a2d2 100644
--- a/src/onelogin/saml2/utils.py
+++ b/src/onelogin/saml2/utils.py
@@ -1127,7 +1127,7 @@ def validate_binary_sign(signed_query, signature, cert=None, algorithm=OneLogin_
return False
@staticmethod
- def get_encoded_parameter(get_data, name, default=None):
+ def get_encoded_parameter(get_data, name, default=None, lowercase_urlencoding=False):
"""Return an url encoded get parameter value
Prefer to extract the original encoded value directly from query_string since url
encoding is not canonical. The encoding used by ADFS 3.0 is not compatible with
@@ -1135,10 +1135,10 @@ def get_encoded_parameter(get_data, name, default=None):
upper case hex numbers)
"""
if name not in get_data:
- return quote_plus(default)
+ return case_sensitive_urlencode(default, lowercase_urlencoding)
if 'query_string' in get_data:
return OneLogin_Saml2_Utils.extract_raw_query_parameter(get_data['query_string'], name)
- return quote_plus(get_data[name])
+ return case_sensitive_urlencode(get_data[name], lowercase_urlencoding)
@staticmethod
def extract_raw_query_parameter(query_string, parameter, default=''):
@@ -1147,3 +1147,8 @@ def extract_raw_query_parameter(query_string, parameter, default=''):
return m.group(1)
else:
return default
+
+ @staticmethod
+ def case_sensitive_urlencode(to_encode, lowercase=False):
+ encoded=quote_plus(to_encode)
+ return re.sub(r"%[A-F0-9]{2}", lambda m: m.group(0).lower(), encoded) if lowercase else encoded
From 37330ba200c17b8389ec06975f6dad64e0bc0e84 Mon Sep 17 00:00:00 2001
From: Max Leblanc
Date: Wed, 25 May 2016 16:27:19 -0400
Subject: [PATCH 023/255] Bugfix
---
src/onelogin/saml2/utils.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py
index 24d2a2d2..fec7ec32 100644
--- a/src/onelogin/saml2/utils.py
+++ b/src/onelogin/saml2/utils.py
@@ -1135,10 +1135,10 @@ def get_encoded_parameter(get_data, name, default=None, lowercase_urlencoding=Fa
upper case hex numbers)
"""
if name not in get_data:
- return case_sensitive_urlencode(default, lowercase_urlencoding)
+ return OneLogin_Saml2_Utils.case_sensitive_urlencode(default, lowercase_urlencoding)
if 'query_string' in get_data:
return OneLogin_Saml2_Utils.extract_raw_query_parameter(get_data['query_string'], name)
- return case_sensitive_urlencode(get_data[name], lowercase_urlencoding)
+ return OneLogin_Saml2_Utils.case_sensitive_urlencode(get_data[name], lowercase_urlencoding)
@staticmethod
def extract_raw_query_parameter(query_string, parameter, default=''):
From 05fd047aed261978dc96ad05a15a2f24d5f36045 Mon Sep 17 00:00:00 2001
From: Max Leblanc
Date: Thu, 26 May 2016 11:55:10 -0400
Subject: [PATCH 024/255] Better solution for the problem
---
src/onelogin/saml2/auth.py | 6 +++---
src/onelogin/saml2/logout_request.py | 8 ++++----
src/onelogin/saml2/logout_response.py | 8 ++++----
src/onelogin/saml2/utils.py | 4 +++-
4 files changed, 14 insertions(+), 12 deletions(-)
diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py
index 5626e8ba..ff06da50 100644
--- a/src/onelogin/saml2/auth.py
+++ b/src/onelogin/saml2/auth.py
@@ -111,7 +111,7 @@ def process_response(self, request_id=None):
OneLogin_Saml2_Error.SAML_RESPONSE_NOT_FOUND
)
- def process_slo(self, keep_local_session=False, request_id=None, delete_session_cb=None, lowercase_urlencoding=False):
+ def process_slo(self, keep_local_session=False, request_id=None, delete_session_cb=None):
"""
Process the SAML Logout Response / Logout Request sent by the IdP.
@@ -127,7 +127,7 @@ def process_slo(self, keep_local_session=False, request_id=None, delete_session_
if 'get_data' in self.__request_data and 'SAMLResponse' in self.__request_data['get_data']:
logout_response = OneLogin_Saml2_Logout_Response(self.__settings, self.__request_data['get_data']['SAMLResponse'])
- if not logout_response.is_valid(self.__request_data, request_id, lowercase_urlencoding=lowercase_urlencoding):
+ if not logout_response.is_valid(self.__request_data, request_id):
self.__errors.append('invalid_logout_response')
self.__error_reason = logout_response.get_error()
elif logout_response.get_status() != OneLogin_Saml2_Constants.STATUS_SUCCESS:
@@ -137,7 +137,7 @@ def process_slo(self, keep_local_session=False, request_id=None, delete_session_
elif 'get_data' in self.__request_data and 'SAMLRequest' in self.__request_data['get_data']:
logout_request = OneLogin_Saml2_Logout_Request(self.__settings, self.__request_data['get_data']['SAMLRequest'])
- if not logout_request.is_valid(self.__request_data, lowercase_urlencoding=lowercase_urlencoding):
+ if not logout_request.is_valid(self.__request_data):
self.__errors.append('invalid_logout_request')
self.__error_reason = logout_request.get_error()
else:
diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py
index 96702b74..8c1a83d2 100644
--- a/src/onelogin/saml2/logout_request.py
+++ b/src/onelogin/saml2/logout_request.py
@@ -251,7 +251,7 @@ def get_session_indexes(request):
session_indexes.append(session_index_node.text)
return session_indexes
- def is_valid(self, request_data, lowercase_urlencoding=False):
+ def is_valid(self, request_data):
"""
Checks if the Logout Request recieved is valid
:param request_data: Request Data
@@ -316,10 +316,10 @@ def is_valid(self, request_data, lowercase_urlencoding=False):
else:
sign_alg = get_data['SigAlg']
- signed_query = 'SAMLRequest=%s' % OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SAMLRequest', lowercase_urlencoding=lowercase_urlencoding)
+ signed_query = 'SAMLRequest=%s' % OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SAMLRequest')
if 'RelayState' in get_data:
- signed_query = '%s&RelayState=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'RelayState', lowercase_urlencoding=lowercase_urlencoding))
- signed_query = '%s&SigAlg=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SigAlg', OneLogin_Saml2_Constants.RSA_SHA1, lowercase_urlencoding=lowercase_urlencoding))
+ signed_query = '%s&RelayState=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'RelayState'))
+ signed_query = '%s&SigAlg=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SigAlg', OneLogin_Saml2_Constants.RSA_SHA1))
if 'x509cert' not in idp_data or idp_data['x509cert'] is None:
raise Exception('In order to validate the sign on the Logout Request, the x509cert of the IdP is required')
diff --git a/src/onelogin/saml2/logout_response.py b/src/onelogin/saml2/logout_response.py
index 7a48aac6..a9cce855 100644
--- a/src/onelogin/saml2/logout_response.py
+++ b/src/onelogin/saml2/logout_response.py
@@ -68,7 +68,7 @@ def get_status(self):
status = entries[0].attrib['Value']
return status
- def is_valid(self, request_data, request_id=None, lowercase_urlencoding=False):
+ def is_valid(self, request_data, request_id=None):
"""
Determines if the SAML LogoutResponse is valid
:param request_id: The ID of the LogoutRequest sent by this SP to the IdP
@@ -119,10 +119,10 @@ def is_valid(self, request_data, request_id=None, lowercase_urlencoding=False):
else:
sign_alg = get_data['SigAlg']
- signed_query = 'SAMLResponse=%s' % OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SAMLResponse', lowercase_urlencoding=lowercase_urlencoding)
+ signed_query = 'SAMLResponse=%s' % OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SAMLResponse')
if 'RelayState' in get_data:
- signed_query = '%s&RelayState=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'RelayState', lowercase_urlencoding=lowercase_urlencoding))
- signed_query = '%s&SigAlg=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SigAlg', OneLogin_Saml2_Constants.RSA_SHA1, lowercase_urlencoding=lowercase_urlencoding))
+ signed_query = '%s&RelayState=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'RelayState'))
+ signed_query = '%s&SigAlg=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SigAlg', OneLogin_Saml2_Constants.RSA_SHA1))
if 'x509cert' not in idp_data or idp_data['x509cert'] is None:
raise Exception('In order to validate the sign on the Logout Response, the x509cert of the IdP is required')
diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py
index fec7ec32..53ac28ff 100644
--- a/src/onelogin/saml2/utils.py
+++ b/src/onelogin/saml2/utils.py
@@ -1127,13 +1127,15 @@ def validate_binary_sign(signed_query, signature, cert=None, algorithm=OneLogin_
return False
@staticmethod
- def get_encoded_parameter(get_data, name, default=None, lowercase_urlencoding=False):
+ def get_encoded_parameter(get_data, name, default=None):
"""Return an url encoded get parameter value
Prefer to extract the original encoded value directly from query_string since url
encoding is not canonical. The encoding used by ADFS 3.0 is not compatible with
python's quote_plus (ADFS produces lower case hex numbers and quote_plus produces
upper case hex numbers)
"""
+ lowercase_urlencoding = get_data.get('lowercase_urlencoding', False)
+
if name not in get_data:
return OneLogin_Saml2_Utils.case_sensitive_urlencode(default, lowercase_urlencoding)
if 'query_string' in get_data:
From 9c72fa3d20c75a9d283ba7e4082e9c0f3021b194 Mon Sep 17 00:00:00 2001
From: Max Leblanc
Date: Thu, 26 May 2016 13:53:48 -0400
Subject: [PATCH 025/255] test
---
src/onelogin/saml2/logout_request.py | 11 ++++++++---
src/onelogin/saml2/logout_response.py | 11 ++++++++---
src/onelogin/saml2/utils.py | 3 +--
3 files changed, 17 insertions(+), 8 deletions(-)
diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py
index 8c1a83d2..a2882ec6 100644
--- a/src/onelogin/saml2/logout_request.py
+++ b/src/onelogin/saml2/logout_request.py
@@ -272,6 +272,11 @@ def is_valid(self, request_data):
else:
get_data = {}
+ if 'lowercase_urlencoding' in request_data.keys():
+ lowercase_urlencoding = request_data['lowercase_urlencoding']
+ else:
+ lowercase_urlencoding = False
+
if self.__settings.is_strict():
res = OneLogin_Saml2_Utils.validate_xml(dom, 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active())
if not isinstance(res, Document):
@@ -316,10 +321,10 @@ def is_valid(self, request_data):
else:
sign_alg = get_data['SigAlg']
- signed_query = 'SAMLRequest=%s' % OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SAMLRequest')
+ signed_query = 'SAMLRequest=%s' % OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SAMLRequest', lowercase_urlencoding=lowercase_urlencoding)
if 'RelayState' in get_data:
- signed_query = '%s&RelayState=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'RelayState'))
- signed_query = '%s&SigAlg=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SigAlg', OneLogin_Saml2_Constants.RSA_SHA1))
+ signed_query = '%s&RelayState=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'RelayState', lowercase_urlencoding=lowercase_urlencoding))
+ signed_query = '%s&SigAlg=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SigAlg', OneLogin_Saml2_Constants.RSA_SHA1, lowercase_urlencoding=lowercase_urlencoding))
if 'x509cert' not in idp_data or idp_data['x509cert'] is None:
raise Exception('In order to validate the sign on the Logout Request, the x509cert of the IdP is required')
diff --git a/src/onelogin/saml2/logout_response.py b/src/onelogin/saml2/logout_response.py
index a9cce855..bcba30f5 100644
--- a/src/onelogin/saml2/logout_response.py
+++ b/src/onelogin/saml2/logout_response.py
@@ -82,6 +82,11 @@ def is_valid(self, request_data, request_id=None):
idp_entity_id = idp_data['entityId']
get_data = request_data['get_data']
+ if 'lowercase_urlencoding' in request_data.keys():
+ lowercase_urlencoding = request_data['lowercase_urlencoding']
+ else:
+ lowercase_urlencoding = False
+
if self.__settings.is_strict():
res = OneLogin_Saml2_Utils.validate_xml(self.document, 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active())
if not isinstance(res, Document):
@@ -119,10 +124,10 @@ def is_valid(self, request_data, request_id=None):
else:
sign_alg = get_data['SigAlg']
- signed_query = 'SAMLResponse=%s' % OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SAMLResponse')
+ signed_query = 'SAMLResponse=%s' % OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SAMLResponse', lowercase_urlencoding=lowercase_urlencoding)
if 'RelayState' in get_data:
- signed_query = '%s&RelayState=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'RelayState'))
- signed_query = '%s&SigAlg=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SigAlg', OneLogin_Saml2_Constants.RSA_SHA1))
+ signed_query = '%s&RelayState=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'RelayState', lowercase_urlencoding=lowercase_urlencoding))
+ signed_query = '%s&SigAlg=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SigAlg', OneLogin_Saml2_Constants.RSA_SHA1, lowercase_urlencoding=lowercase_urlencoding))
if 'x509cert' not in idp_data or idp_data['x509cert'] is None:
raise Exception('In order to validate the sign on the Logout Response, the x509cert of the IdP is required')
diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py
index 53ac28ff..5fbac2d8 100644
--- a/src/onelogin/saml2/utils.py
+++ b/src/onelogin/saml2/utils.py
@@ -1127,14 +1127,13 @@ def validate_binary_sign(signed_query, signature, cert=None, algorithm=OneLogin_
return False
@staticmethod
- def get_encoded_parameter(get_data, name, default=None):
+ def get_encoded_parameter(get_data, name, default=None, lowercase_urlencoding=False):
"""Return an url encoded get parameter value
Prefer to extract the original encoded value directly from query_string since url
encoding is not canonical. The encoding used by ADFS 3.0 is not compatible with
python's quote_plus (ADFS produces lower case hex numbers and quote_plus produces
upper case hex numbers)
"""
- lowercase_urlencoding = get_data.get('lowercase_urlencoding', False)
if name not in get_data:
return OneLogin_Saml2_Utils.case_sensitive_urlencode(default, lowercase_urlencoding)
From 86aaca415ffb949c91437d5e5c88f5ab2ac7c75b Mon Sep 17 00:00:00 2001
From: Max Leblanc
Date: Thu, 26 May 2016 14:31:45 -0400
Subject: [PATCH 026/255] Fixed pep8
---
src/onelogin/saml2/utils.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py
index 5fbac2d8..1d0b80e9 100644
--- a/src/onelogin/saml2/utils.py
+++ b/src/onelogin/saml2/utils.py
@@ -1151,5 +1151,5 @@ def extract_raw_query_parameter(query_string, parameter, default=''):
@staticmethod
def case_sensitive_urlencode(to_encode, lowercase=False):
- encoded=quote_plus(to_encode)
+ encoded = quote_plus(to_encode)
return re.sub(r"%[A-F0-9]{2}", lambda m: m.group(0).lower(), encoded) if lowercase else encoded
From 957b5872dafd62e0067a53ef3b4322c9918e9cca Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Fri, 27 May 2016 22:31:02 +0200
Subject: [PATCH 027/255] Fix #143 READMEs suggest wrong cert name
---
demo-bottle/saml/certs/README | 4 ++--
demo-django/saml/certs/README | 4 ++--
demo-flask/saml/certs/README | 4 ++--
3 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/demo-bottle/saml/certs/README b/demo-bottle/saml/certs/README
index bcb87f11..03c13737 100644
--- a/demo-bottle/saml/certs/README
+++ b/demo-bottle/saml/certs/README
@@ -3,9 +3,9 @@ Take care of this folder that could contain private key. Be sure that this folde
Onelogin Python Toolkit expects that certs for the SP could be stored in this folder as:
* sp.key Private Key
- * sp.cert Public cert
+ * sp.crt Public cert
Also you can use other cert to sign the metadata of the SP using the:
* metadata.key
- * metadata.cert
+ * metadata.crt
diff --git a/demo-django/saml/certs/README b/demo-django/saml/certs/README
index bcb87f11..03c13737 100644
--- a/demo-django/saml/certs/README
+++ b/demo-django/saml/certs/README
@@ -3,9 +3,9 @@ Take care of this folder that could contain private key. Be sure that this folde
Onelogin Python Toolkit expects that certs for the SP could be stored in this folder as:
* sp.key Private Key
- * sp.cert Public cert
+ * sp.crt Public cert
Also you can use other cert to sign the metadata of the SP using the:
* metadata.key
- * metadata.cert
+ * metadata.crt
diff --git a/demo-flask/saml/certs/README b/demo-flask/saml/certs/README
index bcb87f11..03c13737 100644
--- a/demo-flask/saml/certs/README
+++ b/demo-flask/saml/certs/README
@@ -3,9 +3,9 @@ Take care of this folder that could contain private key. Be sure that this folde
Onelogin Python Toolkit expects that certs for the SP could be stored in this folder as:
* sp.key Private Key
- * sp.cert Public cert
+ * sp.crt Public cert
Also you can use other cert to sign the metadata of the SP using the:
* metadata.key
- * metadata.cert
+ * metadata.crt
From f527bc10f25c5675e5ea947382fb71f6715da938 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Fri, 3 Jun 2016 21:21:47 +0200
Subject: [PATCH 028/255] Add parameter to the demos
---
demo-bottle/index.py | 4 +++-
demo-django/demo/views.py | 2 ++
demo-flask/index.py | 2 ++
src/onelogin/saml2/logout_request.py | 3 +--
src/onelogin/saml2/logout_response.py | 3 +--
5 files changed, 9 insertions(+), 5 deletions(-)
diff --git a/demo-bottle/index.py b/demo-bottle/index.py
index 2763f66c..45b234c3 100644
--- a/demo-bottle/index.py
+++ b/demo-bottle/index.py
@@ -30,13 +30,15 @@ def init_saml_auth(req):
def prepare_bottle_request(req):
url_data = urlparse(req.url)
return {
+ 'https': 'on' if req.urlparts.scheme == 'https' else 'off',
'http_host': req.get_header('host'),
'server_port': url_data.port,
'script_name': req.fullpath,
'get_data': req.query,
'post_data': req.forms,
'query_string': req.query_string,
- 'https': 'on' if req.urlparts.scheme == 'https' else 'off'
+ # Uncomment if using ADFS as IdP, https://github.com/onelogin/python-saml/pull/144
+ # 'lowercase_urlencoding': True
}
diff --git a/demo-django/demo/views.py b/demo-django/demo/views.py
index 13881f65..859ff9df 100644
--- a/demo-django/demo/views.py
+++ b/demo-django/demo/views.py
@@ -24,6 +24,8 @@ def prepare_django_request(request):
'server_port': request.META['SERVER_PORT'],
'get_data': request.GET.copy(),
'post_data': request.POST.copy(),
+ # Uncomment if using ADFS as IdP, https://github.com/onelogin/python-saml/pull/144
+ # 'lowercase_urlencoding': True,
'query_string': request.META['QUERY_STRING']
}
return result
diff --git a/demo-flask/index.py b/demo-flask/index.py
index 9c9abf42..9dee0bc8 100644
--- a/demo-flask/index.py
+++ b/demo-flask/index.py
@@ -29,6 +29,8 @@ def prepare_flask_request(request):
'script_name': request.path,
'get_data': request.args.copy(),
'post_data': request.form.copy(),
+ # Uncomment if using ADFS as IdP, https://github.com/onelogin/python-saml/pull/144
+ # 'lowercase_urlencoding': True,
'query_string': request.query_string
}
diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py
index a2882ec6..dc519cec 100644
--- a/src/onelogin/saml2/logout_request.py
+++ b/src/onelogin/saml2/logout_request.py
@@ -261,6 +261,7 @@ def is_valid(self, request_data):
:rtype: boolean
"""
self.__error = None
+ lowercase_urlencoding = False
try:
dom = fromstring(self.__logout_request)
@@ -274,8 +275,6 @@ def is_valid(self, request_data):
if 'lowercase_urlencoding' in request_data.keys():
lowercase_urlencoding = request_data['lowercase_urlencoding']
- else:
- lowercase_urlencoding = False
if self.__settings.is_strict():
res = OneLogin_Saml2_Utils.validate_xml(dom, 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active())
diff --git a/src/onelogin/saml2/logout_response.py b/src/onelogin/saml2/logout_response.py
index bcba30f5..a0f6f25d 100644
--- a/src/onelogin/saml2/logout_response.py
+++ b/src/onelogin/saml2/logout_response.py
@@ -77,6 +77,7 @@ def is_valid(self, request_data, request_id=None):
:rtype: boolean
"""
self.__error = None
+ lowercase_urlencoding = False
try:
idp_data = self.__settings.get_idp_data()
idp_entity_id = idp_data['entityId']
@@ -84,8 +85,6 @@ def is_valid(self, request_data, request_id=None):
if 'lowercase_urlencoding' in request_data.keys():
lowercase_urlencoding = request_data['lowercase_urlencoding']
- else:
- lowercase_urlencoding = False
if self.__settings.is_strict():
res = OneLogin_Saml2_Utils.validate_xml(self.document, 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active())
From bab7412ab5aa8e66af95477fc5869350198c3d94 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Fri, 3 Jun 2016 21:50:07 +0200
Subject: [PATCH 029/255] .
---
demo-bottle/index.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/demo-bottle/index.py b/demo-bottle/index.py
index 45b234c3..f6b5d78a 100644
--- a/demo-bottle/index.py
+++ b/demo-bottle/index.py
@@ -36,9 +36,9 @@ def prepare_bottle_request(req):
'script_name': req.fullpath,
'get_data': req.query,
'post_data': req.forms,
- 'query_string': req.query_string,
# Uncomment if using ADFS as IdP, https://github.com/onelogin/python-saml/pull/144
- # 'lowercase_urlencoding': True
+ # 'lowercase_urlencoding': True,
+ 'query_string': req.query_string
}
From 9d312a87123b2cae42973e73c0cb5ea5ae6f6cd6 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Sat, 4 Jun 2016 00:02:17 +0200
Subject: [PATCH 030/255] Release 2.1.8
---
changelog.md | 9 +++++++++
setup.py | 2 +-
2 files changed, 10 insertions(+), 1 deletion(-)
diff --git a/changelog.md b/changelog.md
index 695d5e38..132e4db4 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,5 +1,14 @@
# python-saml changelog
+### 2.1.8 (Jun 02, 2016)
+* Fix Metadata XML (RequestedAttribute)
+* Fix Windows specific Unix date formatting bug.
+* Docs for OSx instlltion of libsecxml1
+* Fix SHA384 Constant URI
+* [#142](https://github.com/onelogin/python-saml/pull/142) Refactor of settings.py to make it a little more readable.
+* Bugfix for ADFS lowercase signatures
+* READMEs suggested wrong cert name
+
### 2.1.7 (May 14, 2016)
* [#117](https://github.com/onelogin/python-saml/pull/117) AttributeConsumingService support
* [#114](https://github.com/onelogin/python-saml/pull/114) Compare Assertion InResponseTo if not None
diff --git a/setup.py b/setup.py
index 0499d50a..609941f6 100644
--- a/setup.py
+++ b/setup.py
@@ -9,7 +9,7 @@
setup(
name='python-saml',
- version='2.1.7',
+ version='2.1.8',
description='Onelogin Python Toolkit. Add SAML support to your Python software using this library',
classifiers=[
'Development Status :: 4 - Beta',
From 86cb7ef1b819b179d09f77bd6a60df88cc715c46 Mon Sep 17 00:00:00 2001
From: coolbootscoder
Date: Fri, 17 Jun 2016 13:53:25 -0400
Subject: [PATCH 031/255] Changing xmlsec.initialize
---
src/onelogin/saml2/auth.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py
index ff06da50..a04ed50d 100644
--- a/src/onelogin/saml2/auth.py
+++ b/src/onelogin/saml2/auth.py
@@ -433,7 +433,7 @@ def __build_signature(self, saml_data, relay_state, saml_type, sign_algorithm=On
OneLogin_Saml2_Error.SP_CERTS_NOT_FOUND
)
- xmlsec.initialize()
+ xmlsec.initialize('openssl')
dsig_ctx = xmlsec.DSigCtx()
dsig_ctx.signKey = xmlsec.Key.loadMemory(key, xmlsec.KeyDataFormatPem, None)
From 367d8458d79941fd0e4896f730fca0391d06f849 Mon Sep 17 00:00:00 2001
From: coolbootscoder
Date: Fri, 17 Jun 2016 13:54:54 -0400
Subject: [PATCH 032/255] Changing xmlsec.initialize
---
src/onelogin/saml2/utils.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py
index 1d0b80e9..cb15e9e7 100644
--- a/src/onelogin/saml2/utils.py
+++ b/src/onelogin/saml2/utils.py
@@ -626,7 +626,7 @@ def generate_name_id(value, sp_nq, sp_format, cert=None, debug=False, nq=None):
xml = name_id_container.toxml()
elem = fromstring(xml)
- xmlsec.initialize()
+ xmlsec.initialize('openssl')
if debug:
xmlsec.set_error_callback(print_xmlsec_errors)
From d4c82a465dcbafc40e4d8a6e026f6a204858f11e Mon Sep 17 00:00:00 2001
From: coolbootscoder
Date: Fri, 17 Jun 2016 14:28:24 -0400
Subject: [PATCH 033/255] Work-around for xmlsec.initialize problem
---
src/onelogin/saml2/utils.py | 3 +++
1 file changed, 3 insertions(+)
diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py
index cb15e9e7..eed639eb 100644
--- a/src/onelogin/saml2/utils.py
+++ b/src/onelogin/saml2/utils.py
@@ -34,6 +34,9 @@
from onelogin.saml2.constants import OneLogin_Saml2_Constants
from onelogin.saml2.errors import OneLogin_Saml2_Error
+if not globals().get('xmlsec_setup', False):
+ xmlsec.initialize()
+ globals()['xmlsec_setup'] = True
def print_xmlsec_errors(filename, line, func, error_object, error_subject, reason, msg):
"""
From dfd2bc0fd52f31d8b59cfbda9fcdc4b3b96fd505 Mon Sep 17 00:00:00 2001
From: coolbootscoder
Date: Fri, 17 Jun 2016 14:29:41 -0400
Subject: [PATCH 034/255] Work-around for xmlsec.initialize problem
---
src/onelogin/saml2/auth.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py
index a04ed50d..0cbeeaa7 100644
--- a/src/onelogin/saml2/auth.py
+++ b/src/onelogin/saml2/auth.py
@@ -14,14 +14,14 @@
from base64 import b64encode
from urllib import quote_plus
-import dm.xmlsec.binding as xmlsec
+# import dm.xmlsec.binding as xmlsec
from onelogin.saml2.settings import OneLogin_Saml2_Settings
from onelogin.saml2.response import OneLogin_Saml2_Response
from onelogin.saml2.errors import OneLogin_Saml2_Error
from onelogin.saml2.logout_response import OneLogin_Saml2_Logout_Response
from onelogin.saml2.constants import OneLogin_Saml2_Constants
-from onelogin.saml2.utils import OneLogin_Saml2_Utils
+from onelogin.saml2.utils import OneLogin_Saml2_Utils, xmlsec
from onelogin.saml2.logout_request import OneLogin_Saml2_Logout_Request
from onelogin.saml2.authn_request import OneLogin_Saml2_Authn_Request
@@ -433,7 +433,7 @@ def __build_signature(self, saml_data, relay_state, saml_type, sign_algorithm=On
OneLogin_Saml2_Error.SP_CERTS_NOT_FOUND
)
- xmlsec.initialize('openssl')
+ # xmlsec.initialize() # already done in utils module
dsig_ctx = xmlsec.DSigCtx()
dsig_ctx.signKey = xmlsec.Key.loadMemory(key, xmlsec.KeyDataFormatPem, None)
From 5b1410b3b2c30ca3b08ab13ae5573a7942dd5600 Mon Sep 17 00:00:00 2001
From: coolbootscoder
Date: Fri, 17 Jun 2016 14:30:27 -0400
Subject: [PATCH 035/255] Update utils.py
---
src/onelogin/saml2/utils.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py
index eed639eb..a02a65ef 100644
--- a/src/onelogin/saml2/utils.py
+++ b/src/onelogin/saml2/utils.py
@@ -629,7 +629,7 @@ def generate_name_id(value, sp_nq, sp_format, cert=None, debug=False, nq=None):
xml = name_id_container.toxml()
elem = fromstring(xml)
- xmlsec.initialize('openssl')
+ # xmlsec.initialize('openssl') # done when the module is loaded
if debug:
xmlsec.set_error_callback(print_xmlsec_errors)
From d9760c00cdb4b46c0f91b20e90c24b5976386233 Mon Sep 17 00:00:00 2001
From: coolbootscoder
Date: Fri, 17 Jun 2016 14:38:08 -0400
Subject: [PATCH 036/255] Update utils.py
---
src/onelogin/saml2/utils.py | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py
index a02a65ef..665a215d 100644
--- a/src/onelogin/saml2/utils.py
+++ b/src/onelogin/saml2/utils.py
@@ -738,7 +738,7 @@ def decrypt_element(encrypted_data, key, debug=False):
elif isinstance(encrypted_data, basestring):
encrypted_data = fromstring(str(encrypted_data))
- xmlsec.initialize()
+ # xmlsec.initialize() # Initialized at the start of the module
if debug:
xmlsec.set_error_callback(print_xmlsec_errors)
@@ -813,7 +813,7 @@ def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constant
else:
raise Exception('Error parsing xml string')
- xmlsec.initialize()
+ # xmlsec.initialize() # Initialized at the start of the module
if debug:
xmlsec.set_error_callback(print_xmlsec_errors)
@@ -919,7 +919,7 @@ def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', valid
else:
raise Exception('Error parsing xml string')
- xmlsec.initialize()
+ # xmlsec.initialize() # Initialized at the start of the module
if debug:
xmlsec.set_error_callback(print_xmlsec_errors)
@@ -985,7 +985,7 @@ def validate_metadata_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha
else:
raise Exception('Error parsing xml string')
- xmlsec.initialize()
+ # xmlsec.initialize() # Initialized at the start of the module
if debug:
xmlsec.set_error_callback(print_xmlsec_errors)
@@ -1038,7 +1038,7 @@ def validate_node_sign(signature_node, elem, cert=None, fingerprint=None, finger
:type: bool
"""
try:
- xmlsec.initialize()
+ # xmlsec.initialize() # Initialized at the start of the module
if debug:
xmlsec.set_error_callback(print_xmlsec_errors)
@@ -1103,7 +1103,7 @@ def validate_binary_sign(signed_query, signature, cert=None, algorithm=OneLogin_
:type: bool
"""
try:
- xmlsec.initialize()
+ # xmlsec.initialize() # Initialized at the start of the module
if debug:
xmlsec.set_error_callback(print_xmlsec_errors)
From b65749d84fd9dcd6fb428d5548d0d1445b86d6e6 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Mon, 27 Jun 2016 12:24:55 +0200
Subject: [PATCH 037/255] On this commit we changed the way we decrypt an
Assertion and added 2 new validations in order to avoid Signature wrapping
attacks: - Extra validations at the validateSignedElements method to check
that the ref URIs and IDs are unique and consistent. - Validate the
document (encrypted and decrypted version) against the scheme.
---
src/onelogin/saml2/response.py | 70 ++++++++++++++++---
src/onelogin/saml2/utils.py | 1 -
.../src/OneLogin/saml2_tests/response_test.py | 2 +-
3 files changed, 60 insertions(+), 13 deletions(-)
diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py
index 77453b86..de94228b 100644
--- a/src/onelogin/saml2/response.py
+++ b/src/onelogin/saml2/response.py
@@ -86,16 +86,19 @@ def is_valid(self, request_data, request_id=None):
sp_data = self.__settings.get_sp_data()
sp_entity_id = sp_data.get('entityId', '')
- sign_nodes = self.__query('//ds:Signature')
-
- signed_elements = []
- for sign_node in sign_nodes:
- signed_elements.append(sign_node.getparent().tag)
+ signed_elements = self.process_signed_elements()
if self.__settings.is_strict():
+ no_valid_xml_msg = 'Invalid SAML Response. Not match the saml-schema-protocol-2.0.xsd'
res = OneLogin_Saml2_Utils.validate_xml(etree.tostring(self.document), 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active())
if not isinstance(res, Document):
- raise Exception('Invalid SAML Response. Not match the saml-schema-protocol-2.0.xsd')
+ raise Exception(no_valid_xml_msg)
+
+ # If encrypted, check also the decrypted document
+ if self.encrypted:
+ res = OneLogin_Saml2_Utils.validate_xml(etree.tostring(self.decrypted_document), 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active())
+ if not isinstance(res, Document):
+ raise Exception(no_valid_xml_msg)
security = self.__settings.get_security_data()
current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data)
@@ -385,6 +388,53 @@ def validate_num_assertions(self):
assertion_nodes = OneLogin_Saml2_Utils.query(self.document, '//saml:Assertion')
return (len(encrypted_assertion_nodes) + len(assertion_nodes)) == 1
+ def process_signed_elements(self):
+ """
+ Verifies the signature nodes:
+ - Checks that are Response or Assertion
+ - Check that IDs and reference URI are unique and consistent.
+
+ :returns: The signed elements tag names
+ :rtype: list
+ """
+ sign_nodes = self.__query('//ds:Signature')
+
+ signed_elements = []
+ verified_seis = []
+ verified_ids = []
+ response_tag = '{%s}Response' % OneLogin_Saml2_Constants.NS_SAMLP
+ assertion_tag = '{%s}Assertion' % OneLogin_Saml2_Constants.NS_SAML
+
+ for sign_node in sign_nodes:
+ signed_element = sign_node.getparent().tag
+ if signed_element != response_tag and signed_element != assertion_tag:
+ raise Exception('Invalid Signature Element %s SAML Response rejected' % signed_element)
+
+ if not sign_node.getparent().get('ID'):
+ raise Exception('Signed Element must contain an ID. SAML Response rejected')
+
+ id_value = sign_node.getparent().get('ID')
+ if id_value in verified_ids:
+ raise Exception('Duplicated ID. SAML Response rejected')
+ verified_ids.append(id_value)
+
+ # Check that reference URI matches the parent ID and no duplicate References or IDs
+ ref = OneLogin_Saml2_Utils.query(sign_node, './/ds:Reference')
+ if ref:
+ ref = ref[0]
+ if ref.get('URI'):
+ sei = ref.get('URI')[1:]
+
+ if sei != id_value:
+ raise Exception('Found an invalid Signed Element. SAML Response rejected')
+
+ if sei in verified_seis:
+ raise Exception('Duplicated Reference URI. SAML Response rejected')
+ verified_seis.append(sei)
+
+ signed_elements.append(signed_element)
+ return signed_elements
+
def validate_timestamps(self):
"""
Verifies that the document is valid according to Conditions Element
@@ -413,10 +463,7 @@ def __query_assertion(self, xpath_expr):
:returns: The queried nodes
:rtype: list
"""
- if self.encrypted:
- assertion_expr = '/saml:EncryptedAssertion/saml:Assertion'
- else:
- assertion_expr = '/saml:Assertion'
+ assertion_expr = '/saml:Assertion'
signature_expr = '/ds:Signature/ds:SignedInfo/ds:Reference'
signed_assertion_query = '/samlp:Response' + assertion_expr + signature_expr
assertion_reference_nodes = self.__query(signed_assertion_query)
@@ -493,7 +540,8 @@ def __decrypt_assertion(self, dom):
keyinfo.append(encrypted_key[0])
encrypted_data = encrypted_data_nodes[0]
- OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key, debug)
+ decrypted = OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key, debug)
+ dom.replace(encrypted_assertion_nodes[0], decrypted)
return dom
def get_error(self):
diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py
index 1d0b80e9..f8415ef2 100644
--- a/src/onelogin/saml2/utils.py
+++ b/src/onelogin/saml2/utils.py
@@ -926,7 +926,6 @@ def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', valid
signature_nodes = OneLogin_Saml2_Utils.query(elem, '/samlp:Response/ds:Signature')
if not len(signature_nodes) > 0:
- signature_nodes += OneLogin_Saml2_Utils.query(elem, '/samlp:Response/saml:EncryptedAssertion/saml:Assertion/ds:Signature')
signature_nodes += OneLogin_Saml2_Utils.query(elem, '/samlp:Response/saml:Assertion/ds:Signature')
if len(signature_nodes) == 1:
diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py
index 435142a8..08602847 100644
--- a/tests/src/OneLogin/saml2_tests/response_test.py
+++ b/tests/src/OneLogin/saml2_tests/response_test.py
@@ -352,7 +352,7 @@ def testOnlyRetrieveAssertionWithIDThatMatchesSignatureReference(self):
nameid = response.get_nameid()
self.assertNotEqual('root@example.com', nameid)
except:
- self.assertEqual('Signature validation failed. SAML Response rejected', response.get_error())
+ self.assertEqual('Invalid Signature Element {urn:oasis:names:tc:SAML:2.0:metadata}EntityDescriptor SAML Response rejected', response.get_error())
def testDoesNotAllowSignatureWrappingAttack(self):
"""
From 2461c36228505d9894d453a5f3a5ff9405224327 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Mon, 27 Jun 2016 12:26:21 +0200
Subject: [PATCH 038/255] Release 2.1.9
---
README.md | 10 ++++++++--
changelog.md | 4 ++++
setup.py | 2 +-
3 files changed, 13 insertions(+), 3 deletions(-)
diff --git a/README.md b/README.md
index 47af42d2..724f6160 100644
--- a/README.md
+++ b/README.md
@@ -14,8 +14,14 @@ This version supports Python2, exists an alternative version compatible with Pyt
#### Warning ####
-`Please if you are using python-saml < v2.1.6. Update it!
-v2.1.6 includes a security patch that will prevent signature wrapping attacks, older versions are vulnerable.` :exclamation:
+Update python-saml to 2.1.9, this version includes a security patch that contains extra validations that will prevent signature wrapping attacks.
+
+python-saml < v2.1.6 is vulnerable and allows signature wrapping!
+
+
+#### Security Guidelines ####
+
+If you believe you have discovered a security vulnerability in this toolkit, please report it at https://www.onelogin.com/security with a description. We follow responsible disclosure guidelines, and will work with you to quickly find a resolution.
Why add SAML support to my software?
diff --git a/changelog.md b/changelog.md
index 132e4db4..4e518024 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,5 +1,9 @@
# python-saml changelog
+### 2.1.9 (Jun 27, 2016)
+* Change the decrypt assertion process.
+* Add 2 extra validations to prevent Signature wrapping attacks.
+
### 2.1.8 (Jun 02, 2016)
* Fix Metadata XML (RequestedAttribute)
* Fix Windows specific Unix date formatting bug.
diff --git a/setup.py b/setup.py
index 609941f6..e38926a1 100644
--- a/setup.py
+++ b/setup.py
@@ -9,7 +9,7 @@
setup(
name='python-saml',
- version='2.1.8',
+ version='2.1.9',
description='Onelogin Python Toolkit. Add SAML support to your Python software using this library',
classifiers=[
'Development Status :: 4 - Beta',
From e01b1462715ac105bb6132e570be6134c26458dc Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Tue, 28 Jun 2016 21:05:35 +0200
Subject: [PATCH 039/255] Document the wantAssertionsEncrypted parameter
---
README.md | 4 ++++
demo-bottle/saml/advanced_settings.json | 3 ++-
demo-django/saml/advanced_settings.json | 1 +
demo-flask/saml/advanced_settings.json | 1 +
4 files changed, 8 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 724f6160..fe1c9069 100644
--- a/README.md
+++ b/README.md
@@ -360,6 +360,10 @@ In addition to the required settings data (idp, sp), there is extra information
// and elements received by this SP to be signed.
"wantMessagesSigned": false,
+ // this SP to be encrypted.
+ 'wantAssertionsEncrypted' => false,
+
+ // Indicates a requirement for the elements received by
// Indicates a requirement for the elements received by
// this SP to be signed. [Metadata of the SP will offer this info]
"wantAssertionsSigned": false,
diff --git a/demo-bottle/saml/advanced_settings.json b/demo-bottle/saml/advanced_settings.json
index 78e5662c..5b9396ff 100644
--- a/demo-bottle/saml/advanced_settings.json
+++ b/demo-bottle/saml/advanced_settings.json
@@ -9,7 +9,8 @@
"wantAssertionsSigned": false,
"wantNameId" : true,
"wantNameIdEncrypted": false,
- "signatureAlgorithm": "http://www.w3.org/2000/09/xmldsig#rsa-sha1"
+ "wantAssertionsEncrypted": false,
+ "signatureAlgorithm": "http://www.w3.org/2000/09/xmldsig#rsa-sha1"
},
"contactPerson": {
"technical": {
diff --git a/demo-django/saml/advanced_settings.json b/demo-django/saml/advanced_settings.json
index 04bd0fba..ed0ba4ab 100644
--- a/demo-django/saml/advanced_settings.json
+++ b/demo-django/saml/advanced_settings.json
@@ -9,6 +9,7 @@
"wantAssertionsSigned": false,
"wantNameId" : true,
"wantNameIdEncrypted": false,
+ "wantAssertionsEncrypted": false,
"signatureAlgorithm": "http://www.w3.org/2000/09/xmldsig#rsa-sha1"
},
"contactPerson": {
diff --git a/demo-flask/saml/advanced_settings.json b/demo-flask/saml/advanced_settings.json
index 04bd0fba..ed0ba4ab 100644
--- a/demo-flask/saml/advanced_settings.json
+++ b/demo-flask/saml/advanced_settings.json
@@ -9,6 +9,7 @@
"wantAssertionsSigned": false,
"wantNameId" : true,
"wantNameIdEncrypted": false,
+ "wantAssertionsEncrypted": false,
"signatureAlgorithm": "http://www.w3.org/2000/09/xmldsig#rsa-sha1"
},
"contactPerson": {
From 3ade98c643fc5eabe73de8aac2452feee0377c6a Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Tue, 28 Jun 2016 21:07:49 +0200
Subject: [PATCH 040/255] typo
---
README.md | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/README.md b/README.md
index fe1c9069..d49b922f 100644
--- a/README.md
+++ b/README.md
@@ -360,14 +360,14 @@ In addition to the required settings data (idp, sp), there is extra information
// and elements received by this SP to be signed.
"wantMessagesSigned": false,
- // this SP to be encrypted.
- 'wantAssertionsEncrypted' => false,
-
- // Indicates a requirement for the elements received by
// Indicates a requirement for the elements received by
// this SP to be signed. [Metadata of the SP will offer this info]
"wantAssertionsSigned": false,
+ // Indicates a requirement for the
+ // elements received by this SP to be encrypted.
+ 'wantAssertionsEncrypted' => false,
+
// Indicates a requirement for the NameID element on the SAMLResponse
// received by this SP to be present.
"wantNameId": true,
From 040a4a69a68139b867858d226c008d539c174c22 Mon Sep 17 00:00:00 2001
From: coolbootscoder
Date: Sat, 2 Jul 2016 18:54:58 -0400
Subject: [PATCH 041/255] PEP8 changes
Added a new line for PIP8 requirement.
---
src/onelogin/saml2/utils.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py
index 665a215d..c93f1da1 100644
--- a/src/onelogin/saml2/utils.py
+++ b/src/onelogin/saml2/utils.py
@@ -38,6 +38,7 @@
xmlsec.initialize()
globals()['xmlsec_setup'] = True
+
def print_xmlsec_errors(filename, line, func, error_object, error_subject, reason, msg):
"""
Auxiliary method. It override the default xmlsec debug message.
From 0ca31c7c73670eede6d197318af175eaab770dbd Mon Sep 17 00:00:00 2001
From: Sam Clegg
Date: Sun, 3 Jul 2016 10:45:25 -0400
Subject: [PATCH 042/255] Fix some spelling and grammar issues in README.md
Also remove trailing whitespace.
---
README.md | 100 +++++++++++++++++++++++++++---------------------------
1 file changed, 50 insertions(+), 50 deletions(-)
diff --git a/README.md b/README.md
index d49b922f..642a131a 100644
--- a/README.md
+++ b/README.md
@@ -7,10 +7,11 @@

Add SAML support to your Python software using this library.
-Forget those complicated libraries and use that open source library provided
+Forget those complicated libraries and use the open source library provided
and supported by OneLogin Inc.
-This version supports Python2, exists an alternative version compatible with Python 3: [python3-saml](https://github.com/onelogin/python3-saml)
+This version supports Python2. There is a separate version that supports
+Python3: [python3-saml](https://github.com/onelogin/python3-saml).
#### Warning ####
@@ -18,20 +19,18 @@ Update python-saml to 2.1.9, this version includes a security patch that contain
python-saml < v2.1.6 is vulnerable and allows signature wrapping!
-
#### Security Guidelines ####
If you believe you have discovered a security vulnerability in this toolkit, please report it at https://www.onelogin.com/security with a description. We follow responsible disclosure guidelines, and will work with you to quickly find a resolution.
-
Why add SAML support to my software?
------------------------------------
SAML is an XML-based standard for web browser single sign-on and is defined by
-the OASIS Security Services Technical Committee. The standard has been around
+the OASIS Security Services Technical Committee. The standard has been around
since 2002, but lately it is becoming popular due its advantages:
- * **Usability** - One-click access from portals or intranets, deep linking,
+ * **Usability** - One-click access from portals or intranets, deep linking,
password elimination and automatically renewing sessions make life
easier for the user.
* **Security** - Based on strong digital signatures for authentication and
@@ -44,21 +43,21 @@ since 2002, but lately it is becoming popular due its advantages:
* **IT Friendly** - SAML simplifies life for IT because it centralizes
authentication, provides greater visibility and makes directory
integration easier.
- * **Opportunity** - B2B cloud vendor should support SAML to facilitate the
+ * **Opportunity** - B2B cloud vendor should support SAML to facilitate the
integration of their product.
General description
-------------------
-OneLogin's SAML Python toolkit let you build a SP (Service Provider) over
-your Python application and connect it to any IdP (Identity Provider).
+OneLogin's SAML Python toolkit lets you turn you Python application into an SP
+(Service Provider) that can connect to a IdP (Identity Provider).
Supports:
* SSO and SLO (SP-Initiated and IdP-Initiated).
* Assertion and nameId encryption.
- * Assertion signature.
- * Message signature: AuthNRequest, LogoutRequest, LogoutResponses.
+ * Assertion signatures.
+ * Message signatures: AuthNRequest, LogoutRequest, LogoutResponses.
* Enable an Assertion Consumer Service endpoint.
* Enable a Single Logout Service endpoint.
* Publish the SP metadata (which can be signed).
@@ -68,7 +67,7 @@ Key features:
* **saml2int** - Implements the SAML 2.0 Web Browser SSO Profile.
* **Session-less** - Forget those common conflicts between the SP and
the final app, the toolkit delegate session in the final app.
- * **Easy to use** - Programmer will be allowed to code high-level and
+ * **Easy to use** - Programmer will be allowed to code high-level and
low-level programming, 2 easy to use APIs are available.
* **Tested** - Thoroughly tested.
* **Popular** - OneLogin's customers use it. Add easy support to your django/flask/bottle web projects.
@@ -77,7 +76,7 @@ Key features:
Installation
------------
-### Dependences ###
+### Dependencies ###
* python 2.7
* [dm.xmlsec.binding](https://pypi.python.org/pypi/dm.xmlsec.binding) Cython/lxml based binding for the XML security library (depends on python-dev libxml2-dev libxmlsec1-dev)
@@ -87,13 +86,14 @@ Installation
Review the setup.py file to know the version of the library that python-saml is using
-### OSX Dependences ###
+### OSX Dependencies ###
+
* python 2.7
* libxmlsec1
-
+
```sh
- # using brew
- brew install libxmlsec1
+# using brew
+$ brew install libxmlsec1
```
@@ -106,7 +106,7 @@ The toolkit is hosted on github. You can download it from:
* Lastest release: https://github.com/onelogin/python-saml/releases/latest
* Master repo: https://github.com/onelogin/python-saml/tree/master
-Copy the core of the library (src/onelogin/saml2 folder) and merge the setup.py inside the python application. (each application has its structure so take your time to locate the Python SAML toolkit in the best place).
+Copy the core of the library (src/onelogin/saml2 folder) and merge the setup.py inside the python application. (each application has its structure so take your time to locate the Python SAML toolkit in the best place).
#### Option 2. Download from pypi ####
@@ -114,7 +114,7 @@ The toolkit is hosted in pypi, you can find the python-saml package at https://p
You can install it executing:
```
- pip install python-saml
+$ pip install python-saml
```
If you want to know how a project can handle python packages review this [guide](https://packaging.python.org/en/latest/tutorial.html) and review this [sampleproject](https://github.com/pypa/sampleproject)
@@ -123,7 +123,7 @@ If you want to know how a project can handle python packages review this [guide]
Security warning
----------------
-In production, the **strict** parameter MUST be set as **"true"**. Otherwise
+In production, the **strict** parameter MUST be set as **"true"**. Otherwise
your environment is not secure and will be exposed to attacks.
@@ -217,7 +217,7 @@ This is the settings.json file:
```javascript
{
- // If strict is True, then the Python Toolkit will reject unsigned
+ // If strict is True, then the Python Toolkit will reject unsigned
// or unencrypted messages if it expects them to be signed or encrypted.
// Also it will reject the messages if the SAML standard is not strictly
// followed. Destination, NameId, Conditions ... are validated too.
@@ -236,11 +236,11 @@ This is the settings.json file:
// URL Location where the from the IdP will be returned
"url": "https:///?acs",
// SAML protocol binding to be used when returning the
- // message. OneLogin Toolkit supports this endpoint for the
+ // message. OneLogin Toolkit supports this endpoint for the
// HTTP-POST binding only.
"binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
},
- // If you need to specify requested attributes, set a
+ // If you need to specify requested attributes, set a
// attributeConsumingService. nameFormat, attributeValue and
// friendlyName can be omitted
"attributeConsumingService": {
@@ -257,7 +257,7 @@ This is the settings.json file:
]
},
// Specifies info about where and how the message MUST be
- // returned to the requester, in this case our SP.
+ // returned to the requester, in this case our SP.
"singleLogoutService": {
// URL Location where the from the IdP will be returned
"url": "https:///?sls",
@@ -282,7 +282,7 @@ This is the settings.json file:
"entityId": "https://app.onelogin.com/saml/metadata/",
// SSO endpoint info of the IdP. (Authentication Request protocol)
"singleSignOnService": {
- // URL Target of the IdP where the Authentication Request Message
+ // URL Target of the IdP where the Authentication Request Message
// will be sent.
"url": "https://app.onelogin.com/trust/saml2/http-post/sso/",
// SAML protocol binding to be used when returning the
@@ -302,16 +302,16 @@ This is the settings.json file:
// Public x509 certificate of the IdP
"x509cert": ""
/*
- * Instead of use the whole x509cert you can use a fingerprint in order to
+ * Instead of using the whole x509cert you can use a fingerprint in order to
* validate a SAMLResponse, but you will need it to validate LogoutRequest and LogoutResponse using the HTTP-Redirect binding.
- *
+ *
* (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
* let the toolkit know which algorithm was used. Possible values: sha1, sha256, sha384 or sha512
* 'sha1' is the default value.
- *
+ *
* Notice that if you want to validate any SAML Message sent by the HTTP-Redirect binding, you
* will need to provide the whole x509cert.
*/
@@ -321,7 +321,7 @@ This is the settings.json file:
}
```
-In addition to the required settings data (idp, sp), there is extra information that could be defined at advanced_settings.json
+In addition to the required settings data (idp, sp), extra settings can be defined in `advanced_settings.json`:
```javascript
{
@@ -334,15 +334,15 @@ In addition to the required settings data (idp, sp), there is extra information
// will be encrypted.
"nameIdEncrypted": false,
- // Indicates whether the messages sent by this SP
+ // Indicates whether the messages sent by this SP
// will be signed. [Metadata of the SP will offer this info]
"authnRequestsSigned": false,
- // Indicates whether the messages sent by this SP
+ // Indicates whether the messages sent by this SP
// will be signed.
"logoutRequestSigned": false,
- // Indicates whether the messages sent by this SP
+ // Indicates whether the messages sent by this SP
// will be signed.
"logoutResponseSigned": false,
@@ -368,7 +368,7 @@ In addition to the required settings data (idp, sp), there is extra information
// elements received by this SP to be encrypted.
'wantAssertionsEncrypted' => false,
- // Indicates a requirement for the NameID element on the SAMLResponse
+ // Indicates a requirement for the NameID element on the SAMLResponse
// received by this SP to be present.
"wantNameId": true,
@@ -428,7 +428,7 @@ In addition to the required settings data (idp, sp), there is extra information
}
```
-In the security section, you can set the way that the SP will handle the messages and assertions. Contact the admin of the IdP and ask him what the IdP expects, and decide what validations will handle the SP and what requirements the SP will have and communicate them to the IdP's admin too.
+In the security section, you can set the way that the SP will handle the messages and assertions. Contact the admin of the IdP and ask them what the IdP expects, and decide what validations will handle the SP and what requirements the SP will have and communicate them to the IdP's admin too.
Once we know what kind of data could be configured, let's talk about the way settings are handled within the toolkit.
@@ -557,7 +557,7 @@ auth.get_last_request_id()
#### The SP Endpoints ####
-Related to the SP there are 3 important endpoints: The metadata view, the ACS view and the SLS view.
+Related to the SP there are 3 important endpoints: The metadata view, the ACS view and the SLS view.
The toolkit provides examples of those views in the demos, but lets see an example.
***SP Metadata***
@@ -580,7 +580,7 @@ The get_sp_metadata will return the metadata signed or not based on the security
Before the XML metadata is exposed, a check takes place to ensure that the info to be provided is valid.
-Instead of use the Auth object, you can directly use
+Instead of using the Auth object, you can directly use
```
saml_settings = OneLogin_Saml2_Settings(settings=None, custom_base_path=None, sp_validation_only=True)
```
@@ -598,7 +598,7 @@ errors = auth.get_errors()
if not errors:
if auth.is_authenticated():
request.session['samlUserdata'] = auth.get_attributes()
- if 'RelayState' in req['post_data'] and
+ if 'RelayState' in req['post_data'] and
OneLogin_Saml2_Utils.get_self_url(req) != req['post_data']['RelayState']:
auth.redirect_to(req['post_data']['RelayState'])
else:
@@ -636,7 +636,7 @@ If we execute print attributes we could get:
"mail": ["Doe"],
"groups": ["users", "members"]
}
-```
+```
Each attribute name can be used as a key to obtain the value. Every attribute is a list of values. A single-valued attribute is a listy of a single element.
@@ -679,7 +679,7 @@ if not logout_response.is_valid(self.__request_data, request_id):
elif logout_response.get_status() != OneLogin_Saml2_Constants.STATUS_SUCCESS:
self.__errors.append('logout_not_success')
elif not keep_local_session:
- OneLogin_Saml2_Utils.delete_local_session(delete_session_cb)
+ OneLogin_Saml2_Utils.delete_local_session(delete_session_cb)
```
If the SLS endpoints receives an Logout Request, the request is validated, the session is closed and a Logout Response is sent to the SLS endpoint of the IdP.
@@ -723,7 +723,7 @@ In order to send a Logout Request to the IdP:
The Logout Request will be sent signed or unsigned based on the security info of the advanced_settings.json ('logoutRequestSigned').
-The IdP will return the Logout Response through the user's client to the Single Logout Service of the SP.
+The IdP will return the Logout Response through the user's client to the Single Logout Service of the SP.
We can set a 'return_to' url parameter to the logout function and that will be converted as a 'RelayState' parameter:
@@ -734,7 +734,7 @@ auth.logout(return_to=target_url)
Also there are 2 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
+* 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.
@@ -758,7 +758,7 @@ auth = OneLogin_Saml2_Auth(req) # Initialize the SP SAML instance
if 'sso' in request.args: # SSO action (SP-SSO initited). Will send an AuthNRequest to the IdP
return redirect(auth.login())
-elif 'sso2' in request.args: # Another SSO init action
+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
@@ -766,13 +766,13 @@ elif 'slo' in request.args: # SLO action. Will sent a Logout
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
- if len(errors) == 0: # that could took place during the process
+ if len(errors) == 0: # that could took place during the process
if not auth.is_authenticated(): # This check if the response was ok and the user
msg = "Not authenticated" # data retrieved or not (user authenticated)
else:
request.session['samlUserdata'] = auth.get_attributes() # Retrieves user data
self_url = OneLogin_Saml2_Utils.get_self_url(req)
- if 'RelayState' in request.form and self_url != request.form['RelayState']:
+ 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
else: # If there is user data we save that to print it later.
msg = ''
@@ -875,7 +875,7 @@ SAML 2 Logout Response class
* ***get_status*** Gets the Status of the Logout Response.
* ***is_valid*** Determines if the SAML LogoutResponse is valid
* ***build*** Creates a Logout Response object.
-* ***get_response*** Returns a Logout Response object.
+* ***get_response*** Returns a Logout Response object.
* ***get_error*** After execute a validation process, if fails this method returns the cause.
@@ -915,7 +915,7 @@ Configuration of the OneLogin Python Toolkit
A class that contains functionality related to the metadata of the SP
-* ***builder*** Generates the metadata of the SP based on the settings.
+* ***builder*** Generates the metadata of the SP based on the settings.
* ***sign_metadata*** Signs the metadata with the key/cert provided.
* ***add_x509_key_descriptors*** Adds the x509 descriptors (sign/encriptation) to the metadata
@@ -1039,7 +1039,7 @@ The flask project contains:
####SP setup####
-The Onelogin's Python Toolkit allows you to provide the settings info in 2 ways: settings files or define a setting dict. In the demo-flask it used the first method.
+The Onelogin's Python Toolkit allows you to provide the settings info in 2 ways: settings files or define a setting dict. In the demo-flask it used the first method.
In the index.py file we define the app.config['SAML_PATH'], that will target to the 'saml' folder. We require it in order to load the settings files.
@@ -1079,7 +1079,7 @@ To run the demo you need to install the requirements first. Load your
virtualenv and execute:
```
pip install -r demo-django/requirements.txt
-```
+```
This will install django and its dependences. Once it has finished, you have to complete the configuration of the toolkit.
Later, with the virtualenv loaded, you can run the demo like this:
@@ -1112,9 +1112,9 @@ The django project contains:
####SP setup####
-The Onelogin's Python Toolkit allows you to provide the settings info in 2 ways: settings files or define a setting dict. In the demo-django it used the first method.
+The Onelogin's Python Toolkit allows you to provide the settings info in 2 ways: settings files or define a setting dict. In the demo-django it used the first method.
-After set the SAML_FOLDER in the demo/settings.py, the settings of the python toolkit will be loaded on the django web.
+After set the SAML_FOLDER in the demo/settings.py, the settings of the python toolkit will be loaded on the django web.
First we need to edit the saml/settings.json, configure the SP part and review the metadata of the IdP and complete the IdP info. Later edit the saml/advanced_settings.json files and configure the how the toolkit will work. Check the settings section of this document if you have any doubt.
From 543f2af063972f4561dc77b417a3f6abec6f323b Mon Sep 17 00:00:00 2001
From: Sam Clegg
Date: Sun, 3 Jul 2016 10:41:53 -0400
Subject: [PATCH 043/255] Fix crash in flask demo error handling
---
demo-flask/index.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/demo-flask/index.py b/demo-flask/index.py
index 9dee0bc8..ac2c1320 100644
--- a/demo-flask/index.py
+++ b/demo-flask/index.py
@@ -121,7 +121,7 @@ def metadata():
resp = make_response(metadata, 200)
resp.headers['Content-Type'] = 'text/xml'
else:
- resp = make_response(errors.join(', '), 500)
+ resp = make_response(', '.join(errors), 500)
return resp
From 40bae020d49be8be2941b3a839f149c4891b37f8 Mon Sep 17 00:00:00 2001
From: Sam Clegg
Date: Tue, 5 Jul 2016 11:32:55 -0400
Subject: [PATCH 044/255] Update LICENSE to include MIT rather than BSD license
Apparently the MIT license was indented for this
project as per the setup.py file but somehow a
copy of the 3-clause BSD license was included in
the LICENSE file.
---
LICENSE | 41 ++++++++++++++++++++---------------------
1 file changed, 20 insertions(+), 21 deletions(-)
diff --git a/LICENSE b/LICENSE
index 5504f2bb..c0826de8 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,24 +1,23 @@
Copyright (c) 2011-2014, OneLogin, Inc.
-All rights reserved.
-Redistribution and use in source and binary forms, with or without
-modification, are permitted provided that the following conditions are met:
- * Redistributions of source code must retain the above copyright
- notice, this list of conditions and the following disclaimer.
- * Redistributions in binary form must reproduce the above copyright
- notice, this list of conditions and the following disclaimer in the
- documentation and/or other materials provided with the distribution.
- * Neither the name of the nor the
- names of its contributors may be used to endorse or promote products
- derived from this software without specific prior written permission.
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
-ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-DISCLAIMED. IN NO EVENT SHALL ONELOGIN, INC. BE LIABLE FOR ANY
-DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
-(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
-LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
-ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
-SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
From 3c77f552594f39fcf34c9a95ce8f7dccd93b8fcc Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Thu, 7 Jul 2016 17:13:23 +0200
Subject: [PATCH 045/255] Update copyright on LICENSE
---
LICENSE | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/LICENSE b/LICENSE
index c0826de8..dbbca9c6 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2011-2014, OneLogin, Inc.
+Copyright (c) 2010-2016 OneLogin, Inc.
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
From e9165c8e757c22ed281de95d2c9cb61732d8e695 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Tue, 19 Jul 2016 16:53:29 +0200
Subject: [PATCH 046/255] Remove unnecesary comments
---
src/onelogin/saml2/auth.py | 4 ----
src/onelogin/saml2/utils.py | 14 --------------
2 files changed, 18 deletions(-)
diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py
index 0cbeeaa7..f613f0b8 100644
--- a/src/onelogin/saml2/auth.py
+++ b/src/onelogin/saml2/auth.py
@@ -14,8 +14,6 @@
from base64 import b64encode
from urllib import quote_plus
-# import dm.xmlsec.binding as xmlsec
-
from onelogin.saml2.settings import OneLogin_Saml2_Settings
from onelogin.saml2.response import OneLogin_Saml2_Response
from onelogin.saml2.errors import OneLogin_Saml2_Error
@@ -433,8 +431,6 @@ def __build_signature(self, saml_data, relay_state, saml_type, sign_algorithm=On
OneLogin_Saml2_Error.SP_CERTS_NOT_FOUND
)
- # xmlsec.initialize() # already done in utils module
-
dsig_ctx = xmlsec.DSigCtx()
dsig_ctx.signKey = xmlsec.Key.loadMemory(key, xmlsec.KeyDataFormatPem, None)
diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py
index 77ee75cf..f04046b7 100644
--- a/src/onelogin/saml2/utils.py
+++ b/src/onelogin/saml2/utils.py
@@ -630,8 +630,6 @@ def generate_name_id(value, sp_nq, sp_format, cert=None, debug=False, nq=None):
xml = name_id_container.toxml()
elem = fromstring(xml)
- # xmlsec.initialize('openssl') # done when the module is loaded
-
if debug:
xmlsec.set_error_callback(print_xmlsec_errors)
@@ -739,8 +737,6 @@ def decrypt_element(encrypted_data, key, debug=False):
elif isinstance(encrypted_data, basestring):
encrypted_data = fromstring(str(encrypted_data))
- # xmlsec.initialize() # Initialized at the start of the module
-
if debug:
xmlsec.set_error_callback(print_xmlsec_errors)
@@ -814,8 +810,6 @@ def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constant
else:
raise Exception('Error parsing xml string')
- # xmlsec.initialize() # Initialized at the start of the module
-
if debug:
xmlsec.set_error_callback(print_xmlsec_errors)
@@ -920,8 +914,6 @@ def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', valid
else:
raise Exception('Error parsing xml string')
- # xmlsec.initialize() # Initialized at the start of the module
-
if debug:
xmlsec.set_error_callback(print_xmlsec_errors)
@@ -985,8 +977,6 @@ def validate_metadata_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha
else:
raise Exception('Error parsing xml string')
- # xmlsec.initialize() # Initialized at the start of the module
-
if debug:
xmlsec.set_error_callback(print_xmlsec_errors)
@@ -1038,8 +1028,6 @@ def validate_node_sign(signature_node, elem, cert=None, fingerprint=None, finger
:type: bool
"""
try:
- # xmlsec.initialize() # Initialized at the start of the module
-
if debug:
xmlsec.set_error_callback(print_xmlsec_errors)
@@ -1103,8 +1091,6 @@ def validate_binary_sign(signed_query, signature, cert=None, algorithm=OneLogin_
:type: bool
"""
try:
- # xmlsec.initialize() # Initialized at the start of the module
-
if debug:
xmlsec.set_error_callback(print_xmlsec_errors)
From b1d20e7f855c567be37408478c5cc25ffa0717d2 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Wed, 27 Jul 2016 10:40:43 +0200
Subject: [PATCH 047/255] Relax isodate requirement
---
setup.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/setup.py b/setup.py
index e38926a1..e157f95a 100644
--- a/setup.py
+++ b/setup.py
@@ -33,7 +33,7 @@
test_suite='tests',
install_requires=[
'dm.xmlsec.binding==1.3.2',
- 'isodate==0.5.0',
+ 'isodate>=0.5.0',
'defusedxml==0.4.1',
],
extras_require={
From 393f015859c97015e1f24a86c45ceb5826088ab1 Mon Sep 17 00:00:00 2001
From: Oluwafemi Sule
Date: Sun, 31 Jul 2016 11:33:30 +0100
Subject: [PATCH 048/255] Fix typographical errors in docstring
---
src/onelogin/saml2/auth.py | 24 +++++++++++------------
src/onelogin/saml2/authn_request.py | 6 +++---
src/onelogin/saml2/idp_metadata_parser.py | 6 +++---
src/onelogin/saml2/logout_request.py | 6 +++---
src/onelogin/saml2/logout_response.py | 4 ++--
src/onelogin/saml2/metadata.py | 2 +-
src/onelogin/saml2/response.py | 2 +-
src/onelogin/saml2/settings.py | 2 +-
src/onelogin/saml2/utils.py | 24 +++++++++++------------
9 files changed, 38 insertions(+), 38 deletions(-)
diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py
index f613f0b8..1240c738 100644
--- a/src/onelogin/saml2/auth.py
+++ b/src/onelogin/saml2/auth.py
@@ -30,7 +30,7 @@ class OneLogin_Saml2_Auth(object):
This class implements the SP SAML instance.
Defines the methods that you can invoke in your application in
- order to add SAML support (initiates sso, initiates slo, processes a
+ order to add SAML support (initiates SSO, initiates SLO, processes a
SAML Response, a Logout Request or a Logout Response).
"""
@@ -80,7 +80,7 @@ def process_response(self, request_id=None):
"""
Process the SAML Response sent by the IdP.
- :param request_id: Is an optional argumen. Is the ID of the AuthNRequest sent by this SP to the IdP.
+ :param request_id: Is an optional argument. Is the ID of the AuthNRequest sent by this SP to the IdP.
:type request_id: string
:raises: OneLogin_Saml2_Error.SAML_RESPONSE_NOT_FOUND, when a POST with a SAMLResponse is not found
@@ -119,7 +119,7 @@ def process_slo(self, keep_local_session=False, request_id=None, delete_session_
:param request_id: The ID of the LogoutRequest sent by this SP to the IdP
:type request_id: string
- :returns: Redirection url
+ :returns: Redirection URL
"""
self.__errors = []
@@ -168,14 +168,14 @@ def process_slo(self, keep_local_session=False, request_id=None, delete_session_
def redirect_to(self, url=None, parameters={}):
"""
- Redirects the user to the url past by parameter or to the url that we defined in our SSO Request.
+ Redirects the user to the URL passed by parameter or to the URL that we defined in our SSO Request.
:param url: The target URL to redirect the user
:type url: string
- :param parameters: Extra parameters to be passed as part of the url
+ :param parameters: Extra parameters to be passed as part of the URL
:type parameters: dict
- :returns: Redirection url
+ :returns: Redirection URL
"""
if url is None and 'RelayState' in self.__request_data['get_data']:
url = self.__request_data['get_data']['RelayState']
@@ -272,16 +272,16 @@ def login(self, return_to=None, force_authn=False, is_passive=False, set_nameid_
:param return_to: Optional argument. The target URL the user should be redirected to after login.
:type return_to: string
- :param force_authn: Optional argument. When true the AuthNReuqest will set the ForceAuthn='true'.
+ :param force_authn: Optional argument. When true the AuthNRequest will set the ForceAuthn='true'.
:type force_authn: bool
- :param is_passive: Optional argument. When true the AuthNReuqest will set the Ispassive='true'.
+ :param is_passive: Optional argument. When true the AuthNRequest will set the Ispassive='true'.
:type is_passive: bool
- :param set_nameid_policy: Optional argument. When true the AuthNReuqest will set a nameIdPolicy element.
+ :param set_nameid_policy: Optional argument. When true the AuthNRequest will set a nameIdPolicy element.
:type set_nameid_policy: bool
- :returns: Redirection url
+ :returns: Redirection URL
:rtype: string
"""
authn_request = OneLogin_Saml2_Authn_Request(self.__settings, force_authn, is_passive, set_nameid_policy)
@@ -355,7 +355,7 @@ def logout(self, return_to=None, name_id=None, session_index=None, nq=None):
def get_sso_url(self):
"""
- Gets the SSO url.
+ Gets the SSO URL.
:returns: An URL, the SSO endpoint of the IdP
:rtype: string
@@ -365,7 +365,7 @@ def get_sso_url(self):
def get_slo_url(self):
"""
- Gets the SLO url.
+ Gets the SLO URL.
:returns: An URL, the SLO endpoint of the IdP
:rtype: string
diff --git a/src/onelogin/saml2/authn_request.py b/src/onelogin/saml2/authn_request.py
index ea0200e8..25162c51 100644
--- a/src/onelogin/saml2/authn_request.py
+++ b/src/onelogin/saml2/authn_request.py
@@ -29,13 +29,13 @@ def __init__(self, settings, force_authn=False, is_passive=False, set_nameid_pol
:param settings: OSetting data
:type return_to: OneLogin_Saml2_Settings
- :param force_authn: Optional argument. When true the AuthNReuqest will set the ForceAuthn='true'.
+ :param force_authn: Optional argument. When true the AuthNRequest will set the ForceAuthn='true'.
:type force_authn: bool
- :param is_passive: Optional argument. When true the AuthNReuqest will set the Ispassive='true'.
+ :param is_passive: Optional argument. When true the AuthNRequest will set the Ispassive='true'.
:type is_passive: bool
- :param set_nameid_policy: Optional argument. When true the AuthNReuqest will set a nameIdPolicy element.
+ :param set_nameid_policy: Optional argument. When true the AuthNRequest will set a nameIdPolicy element.
:type set_nameid_policy: bool
"""
self.__settings = settings
diff --git a/src/onelogin/saml2/idp_metadata_parser.py b/src/onelogin/saml2/idp_metadata_parser.py
index 44c3d4d5..80b38ec1 100644
--- a/src/onelogin/saml2/idp_metadata_parser.py
+++ b/src/onelogin/saml2/idp_metadata_parser.py
@@ -20,13 +20,13 @@
class OneLogin_Saml2_IdPMetadataParser(object):
"""
- A class that contains methods related to obtain and parse metadata from IdP
+ A class that contain methods related to obtaining and parsing metadata from IdP
"""
@staticmethod
def get_metadata(url):
"""
- Get the metadata XML from the provided URL
+ Gets the metadata XML from the provided URL
:param url: Url where the XML of the Identity Provider Metadata is published.
:type url: string
@@ -55,7 +55,7 @@ def get_metadata(url):
@staticmethod
def parse_remote(url, **kwargs):
"""
- Get the metadata XML from the provided URL and parse it, returning a dict with extracted data
+ Gets the metadata XML from the provided URL and parse it, returning a dict with extracted data
:param url: Url where the XML of the Identity Provider Metadata is published.
:type url: string
diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py
index dc519cec..ab79be0d 100644
--- a/src/onelogin/saml2/logout_request.py
+++ b/src/onelogin/saml2/logout_request.py
@@ -118,7 +118,7 @@ def __init__(self, settings, request=None, name_id=None, session_index=None, nq=
def get_request(self, deflate=True):
"""
- Returns the Logout Request defated, base64encoded
+ Returns the Logout Request deflated, base64encoded
:param deflate: It makes the deflate process optional
:type: bool
:return: Logout Request maybe deflated and base64 encoded
@@ -253,7 +253,7 @@ def get_session_indexes(request):
def is_valid(self, request_data):
"""
- Checks if the Logout Request recieved is valid
+ Checks if the Logout Request received is valid
:param request_data: Request Data
:type request_data: dict
@@ -343,6 +343,6 @@ def is_valid(self, request_data):
def get_error(self):
"""
- After execute a validation process, if fails this method returns the cause
+ After executing a validation process, if it fails this method returns the cause
"""
return self.__error
diff --git a/src/onelogin/saml2/logout_response.py b/src/onelogin/saml2/logout_response.py
index a0f6f25d..5420ebbe 100644
--- a/src/onelogin/saml2/logout_response.py
+++ b/src/onelogin/saml2/logout_response.py
@@ -93,7 +93,7 @@ def is_valid(self, request_data, request_id=None):
security = self.__settings.get_security_data()
- # Check if the InResponseTo of the Logout Response matchs the ID of the Logout Request (requestId) if provided
+ # Check if the InResponseTo of the Logout Response matches the ID of the Logout Request (requestId) if provided
if request_id is not None and self.document.documentElement.hasAttribute('InResponseTo'):
in_response_to = self.document.documentElement.getAttribute('InResponseTo')
if request_id != in_response_to:
@@ -207,6 +207,6 @@ def get_response(self, deflate=True):
def get_error(self):
"""
- After execute a validation process, if fails this method returns the cause
+ After executing a validation process, if it fails this method returns the cause
"""
return self.__error
diff --git a/src/onelogin/saml2/metadata.py b/src/onelogin/saml2/metadata.py
index c8c40b95..8a2ddf1c 100644
--- a/src/onelogin/saml2/metadata.py
+++ b/src/onelogin/saml2/metadata.py
@@ -220,7 +220,7 @@ def sign_metadata(metadata, key, cert, sign_algorithm=OneLogin_Saml2_Constants.R
@staticmethod
def add_x509_key_descriptors(metadata, cert=None):
"""
- Adds the x509 descriptors (sign/encriptation) to the metadata
+ Adds the x509 descriptors (sign/encryption) to the metadata
The same cert will be used for sign/encrypt
:param metadata: SAML Metadata XML
diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py
index de94228b..61224b16 100644
--- a/src/onelogin/saml2/response.py
+++ b/src/onelogin/saml2/response.py
@@ -546,6 +546,6 @@ def __decrypt_assertion(self, dom):
def get_error(self):
"""
- After execute a validation process, if fails this method returns the cause
+ After executing a validation process, if it fails this method returns the cause
"""
return self.__error
diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py
index 4470cc91..8e3b151a 100644
--- a/src/onelogin/saml2/settings.py
+++ b/src/onelogin/saml2/settings.py
@@ -112,7 +112,7 @@ def __init__(self, settings=None, custom_base_path=None, sp_validation_only=Fals
def __load_paths(self, base_path=None):
"""
- Sets the paths of the different folders
+ Set the paths of the different folders
"""
if base_path is None:
base_path = dirname(dirname(dirname(abspath(__file__))))
diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py
index f04046b7..c1d0eff6 100644
--- a/src/onelogin/saml2/utils.py
+++ b/src/onelogin/saml2/utils.py
@@ -41,7 +41,7 @@
def print_xmlsec_errors(filename, line, func, error_object, error_subject, reason, msg):
"""
- Auxiliary method. It override the default xmlsec debug message.
+ Auxiliary method. It overrides the default xmlsec debug message.
"""
info = []
@@ -80,7 +80,7 @@ def decode_base64_and_inflate(value):
@staticmethod
def deflate_and_base64_encode(value):
"""
- Deflates and the base64 encodes a string
+ Deflates and then base64 encodes a string
:param value: The string to deflate and encode
:type value: string
:returns: The deflated and encoded string
@@ -137,13 +137,13 @@ def format_cert(cert, heads=True):
"""
Returns a x509 cert (adding header & footer if required).
- :param cert: A x509 unformated cert
+ :param cert: A x509 unformatted cert
:type: string
:param heads: True if we want to include head and footer
:type: boolean
- :returns: Formated cert
+ :returns: Formatted cert
:rtype: string
"""
x509_cert = cert.replace('\x0D', '')
@@ -170,7 +170,7 @@ def format_private_key(key, heads=True):
:param heads: True if we want to include head and footer
:type: boolean
- :returns: Formated private key
+ :returns: Formatted private key
:rtype: string
"""
private_key = key.replace('\x0D', '')
@@ -576,12 +576,12 @@ def calculate_x509_fingerprint(x509_cert, alg='sha1'):
@staticmethod
def format_finger_print(fingerprint):
"""
- Formates a fingerprint.
+ Formats a fingerprint.
:param fingerprint: fingerprint
:type: string
- :returns: Formated fingerprint
+ :returns: Formatted fingerprint
:rtype: string
"""
formated_fingerprint = fingerprint.replace(':', '')
@@ -1012,7 +1012,7 @@ def validate_node_sign(signature_node, elem, cert=None, fingerprint=None, finger
:param xml: The element we should validate
:type: Document
- :param cert: The pubic cert
+ :param cert: The public cert
:type: string
:param fingerprint: The fingerprint of the public cert
@@ -1072,7 +1072,7 @@ def validate_node_sign(signature_node, elem, cert=None, fingerprint=None, finger
@staticmethod
def validate_binary_sign(signed_query, signature, cert=None, algorithm=OneLogin_Saml2_Constants.RSA_SHA1, debug=False):
"""
- Validates signed bynary data (Used to validate GET Signature).
+ Validates signed binary data (Used to validate GET Signature).
:param signed_query: The element we should validate
:type: string
@@ -1081,7 +1081,7 @@ def validate_binary_sign(signed_query, signature, cert=None, algorithm=OneLogin_
:param signature: The signature that will be validate
:type: string
- :param cert: The pubic cert
+ :param cert: The public cert
:type: string
:param algorithm: Signature algorithm
@@ -1117,8 +1117,8 @@ def validate_binary_sign(signed_query, signature, cert=None, algorithm=OneLogin_
@staticmethod
def get_encoded_parameter(get_data, name, default=None, lowercase_urlencoding=False):
- """Return an url encoded get parameter value
- Prefer to extract the original encoded value directly from query_string since url
+ """Return a URL encoded get parameter value
+ Prefer to extract the original encoded value directly from query_string since URL
encoding is not canonical. The encoding used by ADFS 3.0 is not compatible with
python's quote_plus (ADFS produces lower case hex numbers and quote_plus produces
upper case hex numbers)
From 9ffd521537e012ee428a0724a2fdbc7e440784b7 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Thu, 4 Aug 2016 17:16:45 +0200
Subject: [PATCH 049/255] Fix __build_signature method. If relay_state is null
not be part of the SignQuery
---
src/onelogin/saml2/auth.py | 14 ++++++--------
1 file changed, 6 insertions(+), 8 deletions(-)
diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py
index 1240c738..648e9868 100644
--- a/src/onelogin/saml2/auth.py
+++ b/src/onelogin/saml2/auth.py
@@ -150,8 +150,8 @@ def process_slo(self, keep_local_session=False, request_id=None, delete_session_
parameters = {'SAMLResponse': logout_response}
if 'RelayState' in self.__request_data['get_data']:
parameters['RelayState'] = self.__request_data['get_data']['RelayState']
- else:
- parameters['RelayState'] = OneLogin_Saml2_Utils.get_self_url_no_query(self.__request_data)
+ # else:
+ # parameters['RelayState'] = OneLogin_Saml2_Utils.get_self_url_no_query(self.__request_data)
security = self.__settings.get_security_data()
if 'logoutResponseSigned' in security and security['logoutResponseSigned']:
@@ -434,12 +434,10 @@ def __build_signature(self, saml_data, relay_state, saml_type, sign_algorithm=On
dsig_ctx = xmlsec.DSigCtx()
dsig_ctx.signKey = xmlsec.Key.loadMemory(key, xmlsec.KeyDataFormatPem, None)
- saml_data_str = '%s=%s' % (saml_type, quote_plus(saml_data))
- relay_state_str = 'RelayState=%s' % quote_plus(relay_state)
- alg_str = 'SigAlg=%s' % quote_plus(sign_algorithm)
-
- sign_data = [saml_data_str, relay_state_str, alg_str]
- msg = '&'.join(sign_data)
+ msg = '%s=%s' % (saml_type, quote_plus(saml_data))
+ if relay_state is not None:
+ msg += '&RelayState=%s' % quote_plus(relay_state)
+ msg += '&SigAlg=%s' % quote_plus(sign_algorithm)
# Sign the metadata with our private key.
sign_algorithm_transform_map = {
From 5674f9073e1b50c762b7dafbdc7f2fb1b657b712 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Sat, 6 Aug 2016 16:26:01 +0200
Subject: [PATCH 050/255] Remove download stats from README (not working
anymore)
---
README.md | 1 -
1 file changed, 1 deletion(-)
diff --git a/README.md b/README.md
index 642a131a..7ac999c9 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,6 @@
[](http://travis-ci.org/onelogin/python-saml)
[](https://coveralls.io/r/onelogin/python-saml)
[](https://pypi.python.org/pypi/python-saml)
-

Add SAML support to your Python software using this library.
From 87d4c18865e4997061ec62fd0e8d1e070b92e4e7 Mon Sep 17 00:00:00 2001
From: Jesse Shapiro
Date: Tue, 23 Aug 2016 13:41:11 -0700
Subject: [PATCH 051/255] Adding hooks to retrieve most recent decrypted and
sent documents; updating readme and adding tests
---
README.md | 2 ++
src/onelogin/saml2/auth.py | 27 ++++++++++++++++--
src/onelogin/saml2/authn_request.py | 8 ++++++
src/onelogin/saml2/response.py | 12 ++++++++
...d_valid_encrypted_assertion.xml.base64.xml | 7 +++++
...d_valid_encrypted_assertion.xml.base64.xml | 7 +++++
tests/src/OneLogin/saml2_tests/auth_test.py | 27 ++++++++++++++++++
.../saml2_tests/authn_request_test.py | 28 +++++++++++++++++++
.../src/OneLogin/saml2_tests/response_test.py | 15 +++++++++-
9 files changed, 130 insertions(+), 3 deletions(-)
create mode 100644 tests/data/responses/decrypted_valid_encrypted_assertion.xml.base64.xml
create mode 100644 tests/data/responses/pretty_decrypted_valid_encrypted_assertion.xml.base64.xml
diff --git a/README.md b/README.md
index 7ac999c9..68a1e9fc 100644
--- a/README.md
+++ b/README.md
@@ -823,6 +823,8 @@ Main class of OneLogin Python Toolkit
* ***build_response_signature*** Builds the Signature of the SAML Response.
* ***get_settings*** Returns the settings info.
* ***set_strict*** Set the strict mode active/disable.
+* ***get_last_request_xml*** Returns the most recently-constructed XML request
+* ***get_last_response_xml*** Returns the most recently-decrypted XML response
####OneLogin_Saml2_Auth - authn_request.py####
diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py
index 648e9868..e4693a7d 100644
--- a/src/onelogin/saml2/auth.py
+++ b/src/onelogin/saml2/auth.py
@@ -13,6 +13,7 @@
from base64 import b64encode
from urllib import quote_plus
+from lxml import etree
from onelogin.saml2.settings import OneLogin_Saml2_Settings
from onelogin.saml2.response import OneLogin_Saml2_Response
@@ -57,6 +58,8 @@ def __init__(self, request_data, old_settings=None, custom_base_path=None):
self.__errors = []
self.__error_reason = None
self.__last_request_id = None
+ self.__last_request_xml = None
+ self.__last_response_xml = None
def get_settings(self):
"""
@@ -90,7 +93,7 @@ def process_response(self, request_id=None):
if 'post_data' in self.__request_data and 'SAMLResponse' in self.__request_data['post_data']:
# AuthnResponse -- HTTP_POST Binding
response = OneLogin_Saml2_Response(self.__settings, self.__request_data['post_data']['SAMLResponse'])
-
+ self.__last_response_xml = response.get_xml_document()
if response.is_valid(self.__request_data, request_id):
self.__attributes = response.get_attributes()
self.__nameid = response.get_nameid()
@@ -290,7 +293,7 @@ def login(self, return_to=None, force_authn=False, is_passive=False, set_nameid_
saml_request = authn_request.get_request()
parameters = {'SAMLRequest': saml_request}
-
+ self.__last_request_xml = authn_request.get_request_as_xml()
if return_to is not None:
parameters['RelayState'] = return_to
else:
@@ -451,3 +454,23 @@ def __build_signature(self, saml_data, relay_state, saml_type, sign_algorithm=On
signature = dsig_ctx.signBinary(str(msg), sign_algorithm_transform)
return b64encode(signature)
+
+ def get_last_response_xml(self):
+ """
+ Retrieves the decrypted XML of the last SAML response
+
+ :returns: SAML response XML
+ :rtype: string|None
+ """
+ if self.__last_response_xml:
+ return etree.tostring(self.__last_response_xml, pretty_print=True)
+
+ def get_last_request_xml(self):
+ """
+ Retrieves the raw XML sent in the last SAML request
+
+ :returns: SAML request XML
+ :rtype: string|None
+ """
+ if self.__last_request_xml:
+ return self.__last_request_xml
diff --git a/src/onelogin/saml2/authn_request.py b/src/onelogin/saml2/authn_request.py
index 25162c51..3fa4d0f6 100644
--- a/src/onelogin/saml2/authn_request.py
+++ b/src/onelogin/saml2/authn_request.py
@@ -149,3 +149,11 @@ def get_id(self):
:rtype: string
"""
return self.__id
+
+ def get_request_as_xml(self):
+ """
+ Return the XML document that will be sent as part of the request
+ :return: XML request body
+ :rtype: string
+ """
+ return self.__authn_request
diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py
index 61224b16..d4703c67 100644
--- a/src/onelogin/saml2/response.py
+++ b/src/onelogin/saml2/response.py
@@ -549,3 +549,15 @@ def get_error(self):
After executing a validation process, if it fails this method returns the cause
"""
return self.__error
+
+ def get_xml_document(self):
+ """
+ If necessary, decrypt the XML response document, and return it.
+
+ :return: Decrypted XML response document
+ :rtype: string
+ """
+ if self.encrypted:
+ return self.decrypted_document
+ else:
+ return self.document
diff --git a/tests/data/responses/decrypted_valid_encrypted_assertion.xml.base64.xml b/tests/data/responses/decrypted_valid_encrypted_assertion.xml.base64.xml
new file mode 100644
index 00000000..0237994f
--- /dev/null
+++ b/tests/data/responses/decrypted_valid_encrypted_assertion.xml.base64.xml
@@ -0,0 +1,7 @@
+
+ http://idp.example.com/
+
+
+
+ http://idp.example.com/ _68392312d490db6d355555cfbbd8ec95d746516f60 http://stuff.com/endpoints/metadata.php urn:oasis:names:tc:SAML:2.0:ac:classes:Password test test@example.com test waa2 user admin
+
\ No newline at end of file
diff --git a/tests/data/responses/pretty_decrypted_valid_encrypted_assertion.xml.base64.xml b/tests/data/responses/pretty_decrypted_valid_encrypted_assertion.xml.base64.xml
new file mode 100644
index 00000000..fbc5942f
--- /dev/null
+++ b/tests/data/responses/pretty_decrypted_valid_encrypted_assertion.xml.base64.xml
@@ -0,0 +1,7 @@
+
+ http://idp.example.com/
+
+
+
+ http://idp.example.com/ _68392312d490db6d355555cfbbd8ec95d746516f60 http://stuff.com/endpoints/metadata.php urn:oasis:names:tc:SAML:2.0:ac:classes:Password test test@example.com test waa2 user admin
+
diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py
index 2d296e3c..ce987936 100644
--- a/tests/src/OneLogin/saml2_tests/auth_test.py
+++ b/tests/src/OneLogin/saml2_tests/auth_test.py
@@ -896,6 +896,33 @@ def testBuildResponseSignature(self):
except Exception as e:
self.assertIn("Trying to sign the SAMLResponse but can't load the SP private key", e.message)
+ def testGetLastDecryptedResponse(self):
+ settings = self.loadSettingsJSON()
+ message = self.file_contents(join(self.data_path, 'responses', 'valid_encrypted_assertion.xml.base64'))
+ message_wrapper = {'post_data': {'SAMLResponse': message}}
+ auth = OneLogin_Saml2_Auth(message_wrapper, old_settings=settings)
+ auth.process_response()
+ decrypted_response = self.file_contents(join(self.data_path, 'responses', 'pretty_decrypted_valid_encrypted_assertion.xml.base64.xml'))
+ self.assertEqual(auth.get_last_response_xml(), decrypted_response)
+
+ def testGetLastSentRequest(self):
+ settings = self.loadSettingsJSON()
+ auth = OneLogin_Saml2_Auth({'http_host': 'localhost', 'script_name': 'thing'}, old_settings=settings)
+ auth.login()
+ expectedFragment = (
+ 'Destination="http://idp.example.com/SSOService.php"\n'
+ ' ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"\n'
+ ' AssertionConsumerServiceURL="http://stuff.com/endpoints/endpoints/acs.php"\n'
+ ' >\n'
+ ' http://stuff.com/endpoints/metadata.php \n'
+ ' \n'
+ ' \n'
+ ' urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport \n'
+ ' \n '
+ )
+ self.assertIn(expectedFragment, auth.get_last_request_xml())
if __name__ == '__main__':
if is_running_under_teamcity():
diff --git a/tests/src/OneLogin/saml2_tests/authn_request_test.py b/tests/src/OneLogin/saml2_tests/authn_request_test.py
index 4b241a56..4f298710 100644
--- a/tests/src/OneLogin/saml2_tests/authn_request_test.py
+++ b/tests/src/OneLogin/saml2_tests/authn_request_test.py
@@ -64,6 +64,34 @@ def testCreateRequest(self):
self.assertRegexpMatches(inflated, '^
Date: Mon, 29 Aug 2016 09:47:43 -0400
Subject: [PATCH 052/255] Making certain methods raise exceptions when used
inside application
---
src/onelogin/saml2/response.py | 28 +-
src/onelogin/saml2/utils.py | 281 ++++++++++---------
tests/src/OneLogin/saml2_tests/utils_test.py | 31 ++
3 files changed, 203 insertions(+), 137 deletions(-)
diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py
index 61224b16..3734502e 100644
--- a/src/onelogin/saml2/response.py
+++ b/src/onelogin/saml2/response.py
@@ -16,7 +16,7 @@
from xml.dom.minidom import Document
from onelogin.saml2.constants import OneLogin_Saml2_Constants
-from onelogin.saml2.utils import OneLogin_Saml2_Utils
+from onelogin.saml2.utils import OneLogin_Saml2_Utils, return_false_on_exception
class OneLogin_Saml2_Response(object):
@@ -90,13 +90,21 @@ def is_valid(self, request_data, request_id=None):
if self.__settings.is_strict():
no_valid_xml_msg = 'Invalid SAML Response. Not match the saml-schema-protocol-2.0.xsd'
- res = OneLogin_Saml2_Utils.validate_xml(etree.tostring(self.document), 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active())
+ res = OneLogin_Saml2_Utils.validate_xml(
+ etree.tostring(self.document),
+ 'saml-schema-protocol-2.0.xsd',
+ self.__settings.is_debug_active()
+ )
if not isinstance(res, Document):
raise Exception(no_valid_xml_msg)
# If encrypted, check also the decrypted document
if self.encrypted:
- res = OneLogin_Saml2_Utils.validate_xml(etree.tostring(self.decrypted_document), 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active())
+ res = OneLogin_Saml2_Utils.validate_xml(
+ etree.tostring(self.decrypted_document),
+ 'saml-schema-protocol-2.0.xsd',
+ self.__settings.is_debug_active()
+ )
if not isinstance(res, Document):
raise Exception(no_valid_xml_msg)
@@ -123,8 +131,7 @@ def is_valid(self, request_data, request_id=None):
raise Exception('There is no AttributeStatement on the Response')
# Validates Assertion timestamps
- if not self.validate_timestamps():
- raise Exception('Timing issues (please check your clock settings)')
+ self.validate_timestamps(raise_exceptions=True)
encrypted_attributes_nodes = self.__query_assertion('/saml:AttributeStatement/saml:EncryptedAttribute')
if encrypted_attributes_nodes:
@@ -212,8 +219,7 @@ def is_valid(self, request_data, request_id=None):
document_to_validate = self.decrypted_document
else:
document_to_validate = self.document
- if not OneLogin_Saml2_Utils.validate_sign(document_to_validate, cert, fingerprint, fingerprintalg):
- raise Exception('Signature validation failed. SAML Response rejected')
+ OneLogin_Saml2_Utils.validate_sign(document_to_validate, cert, fingerprint, fingerprintalg, raise_exceptions=True)
else:
raise Exception('No Signature found. SAML Response rejected')
@@ -435,10 +441,14 @@ def process_signed_elements(self):
signed_elements.append(signed_element)
return signed_elements
+ @return_false_on_exception
def validate_timestamps(self):
"""
Verifies that the document is valid according to Conditions Element
+ :param raise_exceptions: Whether to return false on failure or raise an exception
+ :type raise_exceptions: Boolean
+
:returns: True if the condition is valid, False otherwise
:rtype: bool
"""
@@ -448,9 +458,9 @@ def validate_timestamps(self):
nb_attr = conditions_node.get('NotBefore')
nooa_attr = conditions_node.get('NotOnOrAfter')
if nb_attr and OneLogin_Saml2_Utils.parse_SAML_to_time(nb_attr) > OneLogin_Saml2_Utils.now() + OneLogin_Saml2_Constants.ALLOWED_CLOCK_DRIFT:
- return False
+ raise Exception('Could not validate timestamp: not yet valid. Check system clock.')
if nooa_attr and OneLogin_Saml2_Utils.parse_SAML_to_time(nooa_attr) + OneLogin_Saml2_Constants.ALLOWED_CLOCK_DRIFT <= OneLogin_Saml2_Utils.now():
- return False
+ raise Exception('Could not validate timestamp: expired. Check system clock.')
return True
def __query_assertion(self, xpath_expr):
diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py
index c1d0eff6..2610f3dc 100644
--- a/src/onelogin/saml2/utils.py
+++ b/src/onelogin/saml2/utils.py
@@ -25,6 +25,7 @@
from uuid import uuid4
from xml.dom.minidom import Document, Element
from defusedxml.minidom import parseString
+from functools import wraps
import zlib
@@ -39,6 +40,24 @@
globals()['xmlsec_setup'] = True
+def return_false_on_exception(func):
+ """
+ Decorator. When applied to a function, it will, by default, suppress any exceptions
+ raised by that function and return False. It may be overridden by passing a
+ "raise_exceptions" keyword argument when calling the wrapped function.
+ """
+ @wraps(func)
+ def exceptfalse(*args, **kwargs):
+ if not kwargs.pop('raise_exceptions', False):
+ try:
+ return func(*args, **kwargs)
+ except Exception:
+ return False
+ else:
+ return func(*args, **kwargs)
+ return exceptfalse
+
+
def print_xmlsec_errors(filename, line, func, error_object, error_subject, reason, msg):
"""
Auxiliary method. It overrides the default xmlsec debug message.
@@ -866,6 +885,7 @@ def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constant
return newdoc.saveXML(newdoc.firstChild)
@staticmethod
+ @return_false_on_exception
def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', validatecert=False, debug=False):
"""
Validates a signature (Message or Assertion).
@@ -887,53 +907,54 @@ def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', valid
:param debug: Activate the xmlsec debug
:type: bool
+
+ :param raise_exceptions: Whether to return false on failure or raise an exception
+ :type raise_exceptions: Boolean
"""
- try:
- if xml is None or xml == '':
- raise Exception('Empty string supplied as input')
- elif isinstance(xml, etree._Element):
- elem = xml
- elif isinstance(xml, Document):
- xml = xml.toxml()
- elem = fromstring(str(xml))
- elif isinstance(xml, Element):
- xml.setAttributeNS(
- unicode(OneLogin_Saml2_Constants.NS_SAMLP),
- 'xmlns:samlp',
- unicode(OneLogin_Saml2_Constants.NS_SAMLP)
- )
- xml.setAttributeNS(
- unicode(OneLogin_Saml2_Constants.NS_SAML),
- 'xmlns:saml',
- unicode(OneLogin_Saml2_Constants.NS_SAML)
- )
- xml = xml.toxml()
- elem = fromstring(str(xml))
- elif isinstance(xml, basestring):
- elem = fromstring(str(xml))
- else:
- raise Exception('Error parsing xml string')
+ if xml is None or xml == '':
+ raise Exception('Empty string supplied as input')
+ elif isinstance(xml, etree._Element):
+ elem = xml
+ elif isinstance(xml, Document):
+ xml = xml.toxml()
+ elem = fromstring(str(xml))
+ elif isinstance(xml, Element):
+ xml.setAttributeNS(
+ unicode(OneLogin_Saml2_Constants.NS_SAMLP),
+ 'xmlns:samlp',
+ unicode(OneLogin_Saml2_Constants.NS_SAMLP)
+ )
+ xml.setAttributeNS(
+ unicode(OneLogin_Saml2_Constants.NS_SAML),
+ 'xmlns:saml',
+ unicode(OneLogin_Saml2_Constants.NS_SAML)
+ )
+ xml = xml.toxml()
+ elem = fromstring(str(xml))
+ elif isinstance(xml, basestring):
+ elem = fromstring(str(xml))
+ else:
+ raise Exception('Error parsing xml string')
- if debug:
- xmlsec.set_error_callback(print_xmlsec_errors)
+ if debug:
+ xmlsec.set_error_callback(print_xmlsec_errors)
- xmlsec.addIDs(elem, ["ID"])
+ xmlsec.addIDs(elem, ["ID"])
- signature_nodes = OneLogin_Saml2_Utils.query(elem, '/samlp:Response/ds:Signature')
+ signature_nodes = OneLogin_Saml2_Utils.query(elem, '/samlp:Response/ds:Signature')
- if not len(signature_nodes) > 0:
- signature_nodes += OneLogin_Saml2_Utils.query(elem, '/samlp:Response/saml:Assertion/ds:Signature')
+ if not len(signature_nodes) > 0:
+ signature_nodes += OneLogin_Saml2_Utils.query(elem, '/samlp:Response/saml:Assertion/ds:Signature')
- if len(signature_nodes) == 1:
- signature_node = signature_nodes[0]
+ if len(signature_nodes) == 1:
+ signature_node = signature_nodes[0]
- return OneLogin_Saml2_Utils.validate_node_sign(signature_node, elem, cert, fingerprint, fingerprintalg, validatecert, debug)
- else:
- return False
- except Exception:
- return False
+ return OneLogin_Saml2_Utils.validate_node_sign(signature_node, elem, cert, fingerprint, fingerprintalg, validatecert, debug, raise_exceptions=True)
+ else:
+ raise Exception('Expected exactly one signature node; got {}.'.format(len(signature_nodes)))
@staticmethod
+ @return_false_on_exception
def validate_metadata_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', validatecert=False, debug=False):
"""
Validates a signature of a EntityDescriptor.
@@ -955,53 +976,53 @@ def validate_metadata_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha
:param debug: Activate the xmlsec debug
:type: bool
+
+ :param raise_exceptions: Whether to return false on failure or raise an exception
+ :type raise_exceptions: Boolean
"""
- try:
- if xml is None or xml == '':
- raise Exception('Empty string supplied as input')
- elif isinstance(xml, etree._Element):
- elem = xml
- elif isinstance(xml, Document):
- xml = xml.toxml()
- elem = fromstring(str(xml))
- elif isinstance(xml, Element):
- xml.setAttributeNS(
- unicode(OneLogin_Saml2_Constants.NS_MD),
- 'xmlns:md',
- unicode(OneLogin_Saml2_Constants.NS_MD)
- )
- xml = xml.toxml()
- elem = fromstring(str(xml))
- elif isinstance(xml, basestring):
- elem = fromstring(str(xml))
- else:
- raise Exception('Error parsing xml string')
+ if xml is None or xml == '':
+ raise Exception('Empty string supplied as input')
+ elif isinstance(xml, etree._Element):
+ elem = xml
+ elif isinstance(xml, Document):
+ xml = xml.toxml()
+ elem = fromstring(str(xml))
+ elif isinstance(xml, Element):
+ xml.setAttributeNS(
+ unicode(OneLogin_Saml2_Constants.NS_MD),
+ 'xmlns:md',
+ unicode(OneLogin_Saml2_Constants.NS_MD)
+ )
+ xml = xml.toxml()
+ elem = fromstring(str(xml))
+ elif isinstance(xml, basestring):
+ elem = fromstring(str(xml))
+ else:
+ raise Exception('Error parsing xml string')
- if debug:
- xmlsec.set_error_callback(print_xmlsec_errors)
+ if debug:
+ xmlsec.set_error_callback(print_xmlsec_errors)
- xmlsec.addIDs(elem, ["ID"])
+ xmlsec.addIDs(elem, ["ID"])
- signature_nodes = OneLogin_Saml2_Utils.query(elem, '/md:EntitiesDescriptor/ds:Signature')
+ signature_nodes = OneLogin_Saml2_Utils.query(elem, '/md:EntitiesDescriptor/ds:Signature')
- if len(signature_nodes) == 0:
- signature_nodes += OneLogin_Saml2_Utils.query(elem, '/md:EntityDescriptor/ds:Signature')
+ if len(signature_nodes) == 0:
+ signature_nodes += OneLogin_Saml2_Utils.query(elem, '/md:EntityDescriptor/ds:Signature')
- if len(signature_nodes) == 0:
- signature_nodes += OneLogin_Saml2_Utils.query(elem, '/md:EntityDescriptor/md:SPSSODescriptor/ds:Signature')
- signature_nodes += OneLogin_Saml2_Utils.query(elem, '/md:EntityDescriptor/md:IDPSSODescriptor/ds:Signature')
+ if len(signature_nodes) == 0:
+ signature_nodes += OneLogin_Saml2_Utils.query(elem, '/md:EntityDescriptor/md:SPSSODescriptor/ds:Signature')
+ signature_nodes += OneLogin_Saml2_Utils.query(elem, '/md:EntityDescriptor/md:IDPSSODescriptor/ds:Signature')
- if len(signature_nodes) > 0:
- for signature_node in signature_nodes:
- if not OneLogin_Saml2_Utils.validate_node_sign(signature_node, elem, cert, fingerprint, fingerprintalg, validatecert, debug):
- return False
- return True
- else:
- return False
- except Exception:
- return False
+ if len(signature_nodes) > 0:
+ for signature_node in signature_nodes:
+ OneLogin_Saml2_Utils.validate_node_sign(signature_node, elem, cert, fingerprint, fingerprintalg, validatecert, debug, raise_exceptions=True)
+ return True
+ else:
+ raise Exception('Could not validate metadata signature: No signature nodes found.')
@staticmethod
+ @return_false_on_exception
def validate_node_sign(signature_node, elem, cert=None, fingerprint=None, fingerprintalg='sha1', validatecert=False, debug=False):
"""
Validates a signature node.
@@ -1026,50 +1047,54 @@ def validate_node_sign(signature_node, elem, cert=None, fingerprint=None, finger
:param debug: Activate the xmlsec debug
:type: bool
+
+ :param raise_exceptions: Whether to return false on failure or raise an exception
+ :type raise_exceptions: Boolean
"""
- try:
- if debug:
- xmlsec.set_error_callback(print_xmlsec_errors)
+ if debug:
+ xmlsec.set_error_callback(print_xmlsec_errors)
- xmlsec.addIDs(elem, ["ID"])
+ xmlsec.addIDs(elem, ["ID"])
- if (cert is None or cert == '') and fingerprint:
- x509_certificate_nodes = OneLogin_Saml2_Utils.query(signature_node, '//ds:Signature/ds:KeyInfo/ds:X509Data/ds:X509Certificate')
- if len(x509_certificate_nodes) > 0:
- x509_certificate_node = x509_certificate_nodes[0]
- x509_cert_value = x509_certificate_node.text
- x509_fingerprint_value = OneLogin_Saml2_Utils.calculate_x509_fingerprint(x509_cert_value, fingerprintalg)
- if fingerprint == x509_fingerprint_value:
- cert = OneLogin_Saml2_Utils.format_cert(x509_cert_value)
+ if (cert is None or cert == '') and fingerprint:
+ x509_certificate_nodes = OneLogin_Saml2_Utils.query(signature_node, '//ds:Signature/ds:KeyInfo/ds:X509Data/ds:X509Certificate')
+ if len(x509_certificate_nodes) > 0:
+ x509_certificate_node = x509_certificate_nodes[0]
+ x509_cert_value = x509_certificate_node.text
+ x509_fingerprint_value = OneLogin_Saml2_Utils.calculate_x509_fingerprint(x509_cert_value, fingerprintalg)
+ if fingerprint == x509_fingerprint_value:
+ cert = OneLogin_Saml2_Utils.format_cert(x509_cert_value)
- # Check if Reference URI is empty
- # reference_elem = OneLogin_Saml2_Utils.query(signature_node, '//ds:Reference')
- # if len(reference_elem) > 0:
- # if reference_elem[0].get('URI') == '':
- # reference_elem[0].set('URI', '#%s' % signature_node.getparent().get('ID'))
+ # Check if Reference URI is empty
+ # reference_elem = OneLogin_Saml2_Utils.query(signature_node, '//ds:Reference')
+ # if len(reference_elem) > 0:
+ # if reference_elem[0].get('URI') == '':
+ # reference_elem[0].set('URI', '#%s' % signature_node.getparent().get('ID'))
- if cert is None or cert == '':
- return False
+ if cert is None or cert == '':
+ raise Exception('Could not validate node signature: No certificate provided.')
- file_cert = OneLogin_Saml2_Utils.write_temp_file(cert)
+ file_cert = OneLogin_Saml2_Utils.write_temp_file(cert)
- if validatecert:
- mngr = xmlsec.KeysMngr()
- mngr.loadCert(file_cert.name, xmlsec.KeyDataFormatCertPem, xmlsec.KeyDataTypeTrusted)
- dsig_ctx = xmlsec.DSigCtx(mngr)
- else:
- dsig_ctx = xmlsec.DSigCtx()
- dsig_ctx.signKey = xmlsec.Key.load(file_cert.name, xmlsec.KeyDataFormatCertPem, None)
+ if validatecert:
+ mngr = xmlsec.KeysMngr()
+ mngr.loadCert(file_cert.name, xmlsec.KeyDataFormatCertPem, xmlsec.KeyDataTypeTrusted)
+ dsig_ctx = xmlsec.DSigCtx(mngr)
+ else:
+ dsig_ctx = xmlsec.DSigCtx()
+ dsig_ctx.signKey = xmlsec.Key.load(file_cert.name, xmlsec.KeyDataFormatCertPem, None)
- file_cert.close()
+ file_cert.close()
- dsig_ctx.setEnabledKeyData([xmlsec.KeyDataX509])
+ dsig_ctx.setEnabledKeyData([xmlsec.KeyDataX509])
+ try:
dsig_ctx.verify(signature_node)
- return True
except Exception:
- return False
+ raise Exception('Signature validation failed. SAML Response rejected')
+ return True
@staticmethod
+ @return_false_on_exception
def validate_binary_sign(signed_query, signature, cert=None, algorithm=OneLogin_Saml2_Constants.RSA_SHA1, debug=False):
"""
Validates signed binary data (Used to validate GET Signature).
@@ -1089,31 +1114,31 @@ def validate_binary_sign(signed_query, signature, cert=None, algorithm=OneLogin_
:param debug: Activate the xmlsec debug
:type: bool
+
+ :param raise_exceptions: Whether to return false on failure or raise an exception
+ :type raise_exceptions: Boolean
"""
- try:
- if debug:
- xmlsec.set_error_callback(print_xmlsec_errors)
+ if debug:
+ xmlsec.set_error_callback(print_xmlsec_errors)
- dsig_ctx = xmlsec.DSigCtx()
+ dsig_ctx = xmlsec.DSigCtx()
- file_cert = OneLogin_Saml2_Utils.write_temp_file(cert)
- dsig_ctx.signKey = xmlsec.Key.load(file_cert.name, xmlsec.KeyDataFormatCertPem, None)
- file_cert.close()
+ file_cert = OneLogin_Saml2_Utils.write_temp_file(cert)
+ dsig_ctx.signKey = xmlsec.Key.load(file_cert.name, xmlsec.KeyDataFormatCertPem, None)
+ file_cert.close()
- # Sign the metadata with our private key.
- sign_algorithm_transform_map = {
- OneLogin_Saml2_Constants.DSA_SHA1: xmlsec.TransformDsaSha1,
- OneLogin_Saml2_Constants.RSA_SHA1: xmlsec.TransformRsaSha1,
- OneLogin_Saml2_Constants.RSA_SHA256: xmlsec.TransformRsaSha256,
- OneLogin_Saml2_Constants.RSA_SHA384: xmlsec.TransformRsaSha384,
- OneLogin_Saml2_Constants.RSA_SHA512: xmlsec.TransformRsaSha512
- }
- sign_algorithm_transform = sign_algorithm_transform_map.get(algorithm, xmlsec.TransformRsaSha1)
-
- dsig_ctx.verifyBinary(signed_query, sign_algorithm_transform, signature)
- return True
- except Exception:
- return False
+ # Sign the metadata with our private key.
+ sign_algorithm_transform_map = {
+ OneLogin_Saml2_Constants.DSA_SHA1: xmlsec.TransformDsaSha1,
+ OneLogin_Saml2_Constants.RSA_SHA1: xmlsec.TransformRsaSha1,
+ OneLogin_Saml2_Constants.RSA_SHA256: xmlsec.TransformRsaSha256,
+ OneLogin_Saml2_Constants.RSA_SHA384: xmlsec.TransformRsaSha384,
+ OneLogin_Saml2_Constants.RSA_SHA512: xmlsec.TransformRsaSha512
+ }
+ sign_algorithm_transform = sign_algorithm_transform_map.get(algorithm, xmlsec.TransformRsaSha1)
+
+ dsig_ctx.verifyBinary(signed_query, sign_algorithm_transform, signature)
+ return True
@staticmethod
def get_encoded_parameter(get_data, name, default=None, lowercase_urlencoding=False):
diff --git a/tests/src/OneLogin/saml2_tests/utils_test.py b/tests/src/OneLogin/saml2_tests/utils_test.py
index ebd221e8..08e617a1 100644
--- a/tests/src/OneLogin/saml2_tests/utils_test.py
+++ b/tests/src/OneLogin/saml2_tests/utils_test.py
@@ -856,11 +856,18 @@ def testValidateSign(self):
except Exception as e:
self.assertEqual('Error parsing xml string', e.message)
+ with self.assertRaisesRegexp(Exception, 'Empty string supplied as input'):
+ OneLogin_Saml2_Utils.validate_sign('', cert, raise_exceptions=True)
+ with self.assertRaisesRegexp(Exception, 'Error parsing xml string'):
+ OneLogin_Saml2_Utils.validate_sign(1, cert, raise_exceptions=True)
+
# expired cert
xml_metadata_signed = self.file_contents(join(self.data_path, 'metadata', 'signed_metadata_settings1.xml'))
self.assertTrue(OneLogin_Saml2_Utils.validate_metadata_sign(xml_metadata_signed, cert))
# expired cert, verified it
self.assertFalse(OneLogin_Saml2_Utils.validate_metadata_sign(xml_metadata_signed, cert, validatecert=True))
+ with self.assertRaises(Exception):
+ OneLogin_Saml2_Utils.validate_metadata_sign(xml_metadata_signed, cert, validatecert=True, raise_exceptions=True)
xml_metadata_signed_2 = self.file_contents(join(self.data_path, 'metadata', 'signed_metadata_settings2.xml'))
self.assertTrue(OneLogin_Saml2_Utils.validate_metadata_sign(xml_metadata_signed_2, cert_2))
@@ -872,6 +879,8 @@ def testValidateSign(self):
self.assertTrue(OneLogin_Saml2_Utils.validate_sign(xml_response_msg_signed, cert))
# expired cert, verified it
self.assertFalse(OneLogin_Saml2_Utils.validate_sign(xml_response_msg_signed, cert, validatecert=True))
+ with self.assertRaises(Exception):
+ OneLogin_Saml2_Utils.validate_sign(xml_response_msg_signed, cert, validatecert=True, raise_exceptions=True)
# modified cert
other_cert_path = join(dirname(__file__), '..', '..', '..', 'certs')
@@ -880,6 +889,10 @@ def testValidateSign(self):
f.close()
self.assertFalse(OneLogin_Saml2_Utils.validate_sign(xml_response_msg_signed, cert_x))
self.assertFalse(OneLogin_Saml2_Utils.validate_sign(xml_response_msg_signed, cert_x, validatecert=True))
+ with self.assertRaises(Exception):
+ OneLogin_Saml2_Utils.validate_sign(xml_response_msg_signed, cert_x, raise_exceptions=True)
+ with self.assertRaises(Exception):
+ OneLogin_Saml2_Utils.validate_sign(xml_response_msg_signed, cert_x, validatecert=True, raise_exceptions=True)
xml_response_msg_signed_2 = b64decode(self.file_contents(join(self.data_path, 'responses', 'signed_message_response2.xml.base64')))
self.assertTrue(OneLogin_Saml2_Utils.validate_sign(xml_response_msg_signed_2, cert_2))
@@ -893,6 +906,8 @@ def testValidateSign(self):
self.assertTrue(OneLogin_Saml2_Utils.validate_sign(xml_response_assert_signed, cert))
# expired cert, verified it
self.assertFalse(OneLogin_Saml2_Utils.validate_sign(xml_response_assert_signed, cert, validatecert=True))
+ with self.assertRaises(Exception):
+ OneLogin_Saml2_Utils.validate_sign(xml_response_assert_signed, cert, validatecert=True, raise_exceptions=True)
xml_response_assert_signed_2 = b64decode(self.file_contents(join(self.data_path, 'responses', 'signed_assertion_response2.xml.base64')))
self.assertTrue(OneLogin_Saml2_Utils.validate_sign(xml_response_assert_signed_2, cert_2))
@@ -904,6 +919,8 @@ def testValidateSign(self):
self.assertTrue(OneLogin_Saml2_Utils.validate_sign(xml_response_double_signed, cert))
# expired cert, verified it
self.assertFalse(OneLogin_Saml2_Utils.validate_sign(xml_response_double_signed, cert, validatecert=True))
+ with self.assertRaises(Exception):
+ OneLogin_Saml2_Utils.validate_sign(xml_response_double_signed, cert, validatecert=True, raise_exceptions=True)
xml_response_double_signed_2 = b64decode(self.file_contents(join(self.data_path, 'responses', 'double_signed_response2.xml.base64')))
self.assertTrue(OneLogin_Saml2_Utils.validate_sign(xml_response_double_signed_2, cert_2))
@@ -917,32 +934,46 @@ def testValidateSign(self):
dom.firstChild.getAttributeNode('ID').nodeValue = u'_34fg27g212d63k1f923845324475802ac0fc24530b'
# Reference validation failed
self.assertFalse(OneLogin_Saml2_Utils.validate_sign(dom, cert_2))
+ with self.assertRaises(Exception):
+ OneLogin_Saml2_Utils.validate_sign(dom, cert_2, raise_exceptions=True)
invalid_fingerprint = 'afe71c34ef740bc87434be13a2263d31271da1f9'
# Wrong fingerprint
self.assertFalse(OneLogin_Saml2_Utils.validate_metadata_sign(xml_metadata_signed_2, None, invalid_fingerprint))
+ with self.assertRaises(Exception):
+ OneLogin_Saml2_Utils.validate_metadata_sign(xml_metadata_signed_2, None, invalid_fingerprint, raise_exceptions=True)
dom_2 = parseString(xml_response_double_signed_2)
self.assertTrue(OneLogin_Saml2_Utils.validate_sign(dom_2, cert_2))
dom_2.firstChild.firstChild.firstChild.nodeValue = 'https://example.com/other-idp'
# Modified message
self.assertFalse(OneLogin_Saml2_Utils.validate_sign(dom_2, cert_2))
+ with self.assertRaises(Exception):
+ OneLogin_Saml2_Utils.validate_sign(dom_2, cert_2, raise_exceptions=True)
# Try to validate directly the Assertion
dom_3 = parseString(xml_response_double_signed_2)
assert_elem_3 = dom_3.firstChild.firstChild.nextSibling.nextSibling.nextSibling
self.assertFalse(OneLogin_Saml2_Utils.validate_sign(assert_elem_3, cert_2))
+ with self.assertRaises(Exception):
+ OneLogin_Saml2_Utils.validate_sign(assert_elem_3, cert_2, raise_exceptions=True)
# Wrong scheme
no_signed = b64decode(self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_signature.xml.base64')))
self.assertFalse(OneLogin_Saml2_Utils.validate_sign(no_signed, cert))
+ with self.assertRaises(Exception):
+ OneLogin_Saml2_Utils.validate_sign(no_signed, cert, raise_exceptions=True)
no_key = b64decode(self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_key.xml.base64')))
self.assertFalse(OneLogin_Saml2_Utils.validate_sign(no_key, cert))
+ with self.assertRaises(Exception):
+ OneLogin_Saml2_Utils.validate_sign(no_key, cert, raise_exceptions=True)
# Signature Wrapping attack
wrapping_attack1 = b64decode(self.file_contents(join(self.data_path, 'responses', 'invalids', 'signature_wrapping_attack.xml.base64')))
self.assertFalse(OneLogin_Saml2_Utils.validate_sign(wrapping_attack1, cert))
+ with self.assertRaises(Exception):
+ OneLogin_Saml2_Utils.validate_sign(wrapping_attack1, cert, raise_exceptions=True)
if __name__ == '__main__':
From 9289d2632dbcadcefa25bda9c981e39a67f28b2c Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Tue, 20 Sep 2016 12:14:04 +0200
Subject: [PATCH 053/255] Fix 157. Support multiple attributeValues on
RequestedAttribute
---
README.md | 2 +-
src/onelogin/saml2/metadata.py | 25 +++++++++++--------
.../src/OneLogin/saml2_tests/metadata_test.py | 22 ++++++++++++++++
3 files changed, 38 insertions(+), 11 deletions(-)
diff --git a/README.md b/README.md
index 7ac999c9..d4a43275 100644
--- a/README.md
+++ b/README.md
@@ -251,7 +251,7 @@ This is the settings.json file:
"isRequired": false,
"nameFormat": "",
"friendlyName": "",
- "attributeValue": ""
+ "attributeValue": []
}
]
},
diff --git a/src/onelogin/saml2/metadata.py b/src/onelogin/saml2/metadata.py
index 8a2ddf1c..903b9548 100644
--- a/src/onelogin/saml2/metadata.py
+++ b/src/onelogin/saml2/metadata.py
@@ -77,7 +77,6 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N
organization = {}
str_attribute_consuming_service = ''
-
if 'attributeConsumingService' in sp and len(sp['attributeConsumingService']):
attr_cs_desc_str = ''
if "serviceDescription" in sp['attributeConsumingService']:
@@ -94,16 +93,22 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N
if 'friendlyName' in req_attribs.keys() and req_attribs['friendlyName']:
req_attr_nameformat_str = " FriendlyName=\"%s\"" % req_attribs['friendlyName']
if 'isRequired' in req_attribs.keys() and req_attribs['isRequired']:
- req_attr_isrequired_str = " isRequired=\"%s\"" % 'true' if req_attribs['isRequired'] else 'false'
+ req_attr_isrequired_str = " isRequired=\"%s\"" % req_attribs['isRequired']
+
if 'attributeValue' in req_attribs.keys() and req_attribs['attributeValue']:
- req_attr_aux_str = """ >
- """
+
+ requested_attribute = """
""", metadata)
+ def testBuilderAttributeConsumingServiceWithMultipleAttributeValue(self):
+ settings = OneLogin_Saml2_Settings(self.loadSettingsJSON('settings5.json'))
+ sp_data = settings.get_sp_data()
+ security = settings.get_security_data()
+ organization = settings.get_organization()
+ contacts = settings.get_contacts()
+
+ metadata = OneLogin_Saml2_Metadata.builder(
+ sp_data, security['authnRequestsSigned'],
+ security['wantAssertionsSigned'], None, None, contacts,
+ organization
+ )
+ self.assertIn("""
+ Test Service
+ Test Service
+ admin
+
+
+ """, metadata)
+
def testSignMetadata(self):
"""
Tests the signMetadata method of the OneLogin_Saml2_Metadata
From b063d96937726f74ce6b19e26f0c888391133caf Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Tue, 20 Sep 2016 12:20:51 +0200
Subject: [PATCH 054/255] Fix pep8
---
src/onelogin/saml2/metadata.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/onelogin/saml2/metadata.py b/src/onelogin/saml2/metadata.py
index 903b9548..cb92bcf7 100644
--- a/src/onelogin/saml2/metadata.py
+++ b/src/onelogin/saml2/metadata.py
@@ -102,9 +102,9 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N
for attrValue in req_attribs['attributeValue']:
req_attr_aux_str += """
"""
From 6415eca2b880f86be91bd78b13ee5a78cf2f4860 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Tue, 20 Sep 2016 18:35:08 +0200
Subject: [PATCH 055/255] Forgot the setting file
---
tests/settings/settings5.json | 65 +++++++++++++++++++++++++++++++++++
1 file changed, 65 insertions(+)
create mode 100644 tests/settings/settings5.json
diff --git a/tests/settings/settings5.json b/tests/settings/settings5.json
new file mode 100644
index 00000000..e399d217
--- /dev/null
+++ b/tests/settings/settings5.json
@@ -0,0 +1,65 @@
+{
+ "strict": false,
+ "debug": false,
+ "custom_base_path": "../../../tests/data/customPath/",
+ "sp": {
+ "entityId": "http://pytoolkit.com:8000/metadata/",
+ "assertionConsumerService": {
+ "url": "http://pytoolkit.com:8000/?acs"
+ },
+ "attributeConsumingService": {
+ "isDefault": false,
+ "serviceName": "Test Service",
+ "serviceDescription": "Test Service",
+ "requestedAttributes": [ {
+ "name": "userType",
+ "nameFormat": "urn:oasis:names:tc:SAML:2.0:attrname-format:basic",
+ "isRequired": false,
+ "attributeValue": ["userType","admin"]
+ },
+ {
+ "name": "urn:oid:0.9.2342.19200300.100.1.1",
+ "nameFormat": "urn:oasis:names:tc:SAML:2.0:attrname-format:uri",
+ "friendlyName": "uid",
+ "isRequired": false
+ }
+ ]
+ },
+ "singleLogoutService": {
+ "url": "http://pytoolkit.com:8000/?sls"
+ },
+ "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
+ },
+ "idp": {
+ "entityId": "https://pitbulk.no-ip.org/simplesaml/saml2/idp/metadata.php",
+ "singleSignOnService": {
+ "url": "http://pitbulk.no-ip.org/SSOService.php"
+ },
+ "singleLogoutService": {
+ "url": "http://pitbulk.no-ip.org/SingleLogoutService.php"
+ },
+ "x509cert": "MIICbDCCAdWgAwIBAgIBADANBgkqhkiG9w0BAQ0FADBTMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRgwFgYDVQQDDA9pZHAuZXhhbXBsZS5jb20wHhcNMTQwOTIzMTIyNDA4WhcNNDIwMjA4MTIyNDA4WjBTMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRgwFgYDVQQDDA9pZHAuZXhhbXBsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAOWA+YHU7cvPOrBOfxCscsYTJB+kH3MaA9BFrSHFS+KcR6cw7oPSktIJxUgvDpQbtfNcOkE/tuOPBDoech7AXfvH6d7Bw7xtW8PPJ2mB5Hn/HGW2roYhxmfh3tR5SdwN6i4ERVF8eLkvwCHsNQyK2Ref0DAJvpBNZMHCpS24916/AgMBAAGjUDBOMB0GA1UdDgQWBBQ77/qVeiigfhYDITplCNtJKZTM8DAfBgNVHSMEGDAWgBQ77/qVeiigfhYDITplCNtJKZTM8DAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBDQUAA4GBAJO2j/1uO80E5C2PM6Fk9mzerrbkxl7AZ/mvlbOn+sNZE+VZ1AntYuG8ekbJpJtG1YfRfc7EA9mEtqvv4dhv7zBy4nK49OR+KpIBjItWB5kYvrqMLKBa32sMbgqqUqeF1ENXKjpvLSuPdfGJZA3dNa/+Dyb8GGqWe707zLyc5F8m"
+ },
+ "security": {
+ "authnRequestsSigned": false,
+ "wantAssertionsSigned": false,
+ "signMetadata": false
+ },
+ "contactPerson": {
+ "technical": {
+ "givenName": "technical_name",
+ "emailAddress": "technical@example.com"
+ },
+ "support": {
+ "givenName": "support_name",
+ "emailAddress": "support@example.com"
+ }
+ },
+ "organization": {
+ "en-US": {
+ "name": "sp_test",
+ "displayname": "SP test",
+ "url": "http://sp.example.com"
+ }
+ }
+}
From f965e5ba1e30ebca1bd200901155c9882b5a5de9 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Wed, 21 Sep 2016 14:00:00 +0200
Subject: [PATCH 056/255] Fix minor bug with AttributeConsumingService. Update
Travis config to test python 2.7.6 and 2.7.12
---
.travis.yml | 3 ++-
src/onelogin/saml2/metadata.py | 5 +++--
tests/src/OneLogin/saml2_tests/metadata_test.py | 6 +++---
3 files changed, 8 insertions(+), 6 deletions(-)
diff --git a/.travis.yml b/.travis.yml
index 0ddaacc3..b287ebb1 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,6 +1,7 @@
language: python
python:
- - '2.7'
+ - '2.7.6'
+ - '2.7.12'
install:
- sudo apt-get update -qq
diff --git a/src/onelogin/saml2/metadata.py b/src/onelogin/saml2/metadata.py
index cb92bcf7..f2932067 100644
--- a/src/onelogin/saml2/metadata.py
+++ b/src/onelogin/saml2/metadata.py
@@ -96,12 +96,13 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N
req_attr_isrequired_str = " isRequired=\"%s\"" % req_attribs['isRequired']
if 'attributeValue' in req_attribs.keys() and req_attribs['attributeValue']:
- req_attr_aux_str = ""
if isinstance(req_attribs['attributeValue'], basestring):
req_attribs['attributeValue'] = [req_attribs['attributeValue']]
+
+ req_attr_aux_str = ">"
for attrValue in req_attribs['attributeValue']:
req_attr_aux_str += """
- %(attributeValue)s """ % \
{
'attributeValue': attrValue
}
diff --git a/tests/src/OneLogin/saml2_tests/metadata_test.py b/tests/src/OneLogin/saml2_tests/metadata_test.py
index 0fd3a18a..6d7ca494 100644
--- a/tests/src/OneLogin/saml2_tests/metadata_test.py
+++ b/tests/src/OneLogin/saml2_tests/metadata_test.py
@@ -180,9 +180,9 @@ def testBuilderAttributeConsumingServiceWithMultipleAttributeValue(self):
self.assertIn("""
Test Service
Test Service
- admin
+
+ userType
+ admin
""", metadata)
From abcbdd8ae60a7e279602eca35c0a890af25185b6 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Wed, 21 Sep 2016 14:20:03 +0200
Subject: [PATCH 057/255] Deactivate Travis on python 2.7.12
---
.travis.yml | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/.travis.yml b/.travis.yml
index b287ebb1..41d26230 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,7 +1,8 @@
language: python
python:
- '2.7.6'
- - '2.7.12'
+ - '2.7.9'
+# - '2.7.12'
install:
- sudo apt-get update -qq
From 6f36e597df97072e5b5f2800596eb835adf447da Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Wed, 21 Sep 2016 14:27:51 +0200
Subject: [PATCH 058/255] .
---
.travis.yml | 1 -
1 file changed, 1 deletion(-)
diff --git a/.travis.yml b/.travis.yml
index 41d26230..4f275e8d 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,7 +1,6 @@
language: python
python:
- '2.7.6'
- - '2.7.9'
# - '2.7.12'
install:
From 4e73f1756ab0461ed67edfdcaeb17c0d58671d84 Mon Sep 17 00:00:00 2001
From: Matjaz Gregoric
Date: Wed, 21 Sep 2016 10:50:07 +0200
Subject: [PATCH 059/255] Add support for non-ascii fields in settings.
---
src/onelogin/saml2/metadata.py | 4 +-
src/onelogin/saml2/utils.py | 32 ++++++-------
tests/settings/settings6.json | 47 +++++++++++++++++++
tests/src/OneLogin/saml2_tests/auth_test.py | 18 ++++++-
.../src/OneLogin/saml2_tests/settings_test.py | 18 ++++++-
tests/src/OneLogin/saml2_tests/utils_test.py | 12 +++++
6 files changed, 109 insertions(+), 22 deletions(-)
create mode 100644 tests/settings/settings6.json
diff --git a/src/onelogin/saml2/metadata.py b/src/onelogin/saml2/metadata.py
index f2932067..6ac03c27 100644
--- a/src/onelogin/saml2/metadata.py
+++ b/src/onelogin/saml2/metadata.py
@@ -173,7 +173,7 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N
contacts_info.append(contact)
str_contacts = '\n'.join(contacts_info) + '\n'
- metadata = """
+ metadata = u"""
', metadata)
self.assertIn('urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified ', metadata)
+ def testGetUnicodeSPMetadata(self):
+ """
+ Tests the getSPMetadata method of the OneLogin_Saml2_Settings
+ Case unicode metadata
+ """
+ settings = OneLogin_Saml2_Settings(self.loadSettingsJSON('settings6.json'))
+ metadata = settings.get_sp_metadata()
+
+ self.assertIn(' ', metadata)
+ self.assertIn(u'Sérvïçé prövïdér ', metadata)
+ self.assertIn(u'Téçhnïçäl Nämé ', metadata)
+ self.assertIn(u'Süppört Nämé ', metadata)
+
def testGetSPMetadataSigned(self):
"""
Tests the getSPMetadata method of the OneLogin_Saml2_Settings
diff --git a/tests/src/OneLogin/saml2_tests/utils_test.py b/tests/src/OneLogin/saml2_tests/utils_test.py
index ebd221e8..cb0a2a3d 100644
--- a/tests/src/OneLogin/saml2_tests/utils_test.py
+++ b/tests/src/OneLogin/saml2_tests/utils_test.py
@@ -39,6 +39,18 @@ def file_contents(self, filename):
f.close()
return content
+ def testDeflateBase64Roundtrip(self):
+ """
+ Tests deflate_and_base64_encode and decode_base64_and_inflate methods of OneLogin_Saml2_Utils
+ """
+ body = 'Some random string.'
+ encoded = OneLogin_Saml2_Utils.deflate_and_base64_encode(body)
+ self.assertEqual(OneLogin_Saml2_Utils.decode_base64_and_inflate(encoded), body)
+
+ unicode_body = u'Sömé rändöm nön-äsçïï strïng.'
+ unicode_encoded = OneLogin_Saml2_Utils.deflate_and_base64_encode(unicode_body)
+ self.assertEqual(OneLogin_Saml2_Utils.decode_base64_and_inflate(unicode_encoded), unicode_body)
+
def testValidateXML(self):
"""
Tests the validate_xml method of the OneLogin_Saml2_Utils
From aeb25be9aff1313ec87c2f9b19687fb76088813f Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Thu, 6 Oct 2016 10:21:44 +0200
Subject: [PATCH 060/255] =?UTF-8?q?Several=20security=20improvements:=20-?=
=?UTF-8?q?=20Conditions=20element=20required=20and=20unique.=20-=20AuthnS?=
=?UTF-8?q?tatement=20element=20required=20and=20unique.=20-=20SPNameQuali?=
=?UTF-8?q?fier=20must=20math=20the=20SP=20EntityID=20-=20Reject=20saml:At?=
=?UTF-8?q?tribute=20element=20with=20same=20=E2=80=9CName=E2=80=9D=20attr?=
=?UTF-8?q?ibute=20-=20Reject=20empty=20nameID=20-=20Require=20Issuer=20el?=
=?UTF-8?q?ement.=20(Must=20match=20IdP=20EntityID).=20-=20Destination=20v?=
=?UTF-8?q?alue=20can't=20be=20blank=20(if=20present=20must=20match=20ACS?=
=?UTF-8?q?=20URL).=20-=20Check=20that=20the=20EncryptedAssertion=20elemen?=
=?UTF-8?q?t=20only=20contains=201=20Assertion=20element.?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/onelogin/saml2/response.py | 71 ++++++++--
src/onelogin/saml2/utils.py | 15 +-
.../invalids/duplicated_attributes.xml.base64 | 1 +
.../invalids/empty_destination.xml.base64 | 1 +
.../invalids/empty_nameid.xml.base64 | 1 +
.../invalids/no_authnstatement.xml.base64 | 1 +
.../invalids/no_conditions.xml.base64 | 1 +
.../invalids/no_issuer_assertion.xml.base64 | 1 +
.../invalids/no_issuer_response.xml.base64 | 1 +
.../invalids/wrong_spnamequalifier.xml.base64 | 1 +
.../response_encrypted_nameid.xml.base64 | 2 +-
.../data/responses/valid_response.xml.base64 | 2 +-
.../src/OneLogin/saml2_tests/response_test.py | 130 ++++++++++++++++--
tests/src/OneLogin/saml2_tests/utils_test.py | 4 +-
14 files changed, 202 insertions(+), 30 deletions(-)
create mode 100644 tests/data/responses/invalids/duplicated_attributes.xml.base64
create mode 100644 tests/data/responses/invalids/empty_destination.xml.base64
create mode 100644 tests/data/responses/invalids/empty_nameid.xml.base64
create mode 100644 tests/data/responses/invalids/no_authnstatement.xml.base64
create mode 100644 tests/data/responses/invalids/no_conditions.xml.base64
create mode 100644 tests/data/responses/invalids/no_issuer_assertion.xml.base64
create mode 100644 tests/data/responses/invalids/no_issuer_response.xml.base64
create mode 100644 tests/data/responses/invalids/wrong_spnamequalifier.xml.base64
diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py
index 61224b16..b8aac5b3 100644
--- a/src/onelogin/saml2/response.py
+++ b/src/onelogin/saml2/response.py
@@ -114,24 +114,32 @@ def is_valid(self, request_data, request_id=None):
if security.get('wantNameIdEncrypted', False):
encrypted_nameid_nodes = self.__query_assertion('/saml:Subject/saml:EncryptedID/xenc:EncryptedData')
- if len(encrypted_nameid_nodes) == 0:
+ if len(encrypted_nameid_nodes) != 1:
raise Exception('The NameID of the Response is not encrypted and the SP require it')
- # Checks that there is at least one AttributeStatement if required
- attribute_statement_nodes = self.__query_assertion('/saml:AttributeStatement')
- if security.get('wantAttributeStatement', True) and not attribute_statement_nodes:
- raise Exception('There is no AttributeStatement on the Response')
+ # Checks that a Conditions element exists
+ if not self.check_one_condition():
+ raise Exception('The Assertion must include a Conditions element')
# Validates Assertion timestamps
if not self.validate_timestamps():
raise Exception('Timing issues (please check your clock settings)')
+ # Checks that an AuthnStatement element exists and is unique
+ if not self.check_one_authnstatement():
+ raise Exception('The Assertion must include an AuthnStatement element')
+
+ # Checks that there is at least one AttributeStatement if required
+ attribute_statement_nodes = self.__query_assertion('/saml:AttributeStatement')
+ if security.get('wantAttributeStatement', True) and not attribute_statement_nodes:
+ raise Exception('There is no AttributeStatement on the Response')
+
encrypted_attributes_nodes = self.__query_assertion('/saml:AttributeStatement/saml:EncryptedAttribute')
if encrypted_attributes_nodes:
raise Exception('There is an EncryptedAttribute in the Response and this SP not support them')
# Checks destination
- destination = self.document.get('Destination', '')
+ destination = self.document.get('Destination', None)
if destination:
if not destination.startswith(current_url):
# TODO: Review if following lines are required, since we can control the
@@ -139,6 +147,8 @@ def is_valid(self, request_data, request_id=None):
# current_url_routed = OneLogin_Saml2_Utils.get_self_routed_url_no_query(request_data)
# if not destination.startswith(current_url_routed):
raise Exception('The response was received at %s instead of %s' % (current_url, destination))
+ elif destination == '':
+ raise Exception('The response has an empty Destination value')
# Checks audience
valid_audiences = self.get_audiences()
@@ -242,6 +252,26 @@ def check_status(self):
status_exception_msg += ' -> ' + status_msg
raise Exception(status_exception_msg)
+ def check_one_condition(self):
+ """
+ Checks that the samlp:Response/saml:Assertion/saml:Conditions element exists and is unique.
+ """
+ condition_nodes = self.__query_assertion('/saml:Conditions')
+ if len(condition_nodes) == 1:
+ return True
+ else:
+ return False
+
+ def check_one_authnstatement(self):
+ """
+ Checks that the samlp:Response/saml:Assertion/saml:AuthnStatement element exists and is unique.
+ """
+ authnstatement_nodes = self.__query_assertion('/saml:AuthnStatement')
+ if len(authnstatement_nodes) == 1:
+ return True
+ else:
+ return False
+
def get_audiences(self):
"""
Gets the audiences
@@ -262,12 +292,16 @@ def get_issuers(self):
issuers = []
message_issuer_nodes = self.__query('/samlp:Response/saml:Issuer')
- if message_issuer_nodes:
+ if len(message_issuer_nodes) == 1:
issuers.append(message_issuer_nodes[0].text)
+ else:
+ raise Exception('Issuer of the Response not found or multiple.')
assertion_issuer_nodes = self.__query_assertion('/saml:Issuer')
- if assertion_issuer_nodes:
+ if len(assertion_issuer_nodes) == 1:
issuers.append(assertion_issuer_nodes[0].text)
+ else:
+ raise Exception('Issuer of the Assertion not found or multiple.')
return list(set(issuers))
@@ -296,10 +330,19 @@ def get_nameid_data(self):
if security.get('wantNameId', True):
raise Exception('Not NameID found in the assertion of the Response')
else:
+ if self.__settings.is_strict() and not nameid.text:
+ raise Exception('An empty NameID value found')
+
nameid_data = {'Value': nameid.text}
for attr in ['Format', 'SPNameQualifier', 'NameQualifier']:
value = nameid.get(attr, None)
if value:
+ if self.__settings.is_strict() and attr == 'SPNameQualifier':
+ sp_data = self.__settings.get_sp_data()
+ sp_entity_id = sp_data.get('entityId', '')
+ if sp_entity_id != value:
+ raise Exception('The SPNameQualifier value mistmatch the SP entityID value.')
+
nameid_data[attr] = value
return nameid_data
@@ -355,6 +398,9 @@ def get_attributes(self):
attribute_nodes = self.__query_assertion('/saml:AttributeStatement/saml:Attribute')
for attribute_node in attribute_nodes:
attr_name = attribute_node.get('Name')
+ if attr_name in attributes.keys():
+ raise Exception('Found an Attribute element with duplicated Name')
+
values = []
for attr in attribute_node.iterchildren('{%s}AttributeValue' % OneLogin_Saml2_Constants.NSMAP['saml']):
# Remove any whitespace (which may be present where attributes are
@@ -386,7 +432,14 @@ def validate_num_assertions(self):
"""
encrypted_assertion_nodes = OneLogin_Saml2_Utils.query(self.document, '//saml:EncryptedAssertion')
assertion_nodes = OneLogin_Saml2_Utils.query(self.document, '//saml:Assertion')
- return (len(encrypted_assertion_nodes) + len(assertion_nodes)) == 1
+
+ valid = len(encrypted_assertion_nodes) + len(assertion_nodes) == 1
+
+ if (self.encrypted):
+ assertion_nodes = OneLogin_Saml2_Utils.query(self.decrypted_document, '//saml:Assertion')
+ valid = valid and len(assertion_nodes) == 1
+
+ return valid
def process_signed_elements(self):
"""
diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py
index ee89d59f..fbacf0ef 100644
--- a/src/onelogin/saml2/utils.py
+++ b/src/onelogin/saml2/utils.py
@@ -694,23 +694,22 @@ def get_status(dom):
status = {}
status_entry = OneLogin_Saml2_Utils.query(dom, '/samlp:Response/samlp:Status')
- if len(status_entry) == 0:
- raise Exception('Missing Status on response')
+ if len(status_entry) != 1:
+ raise Exception('Missing valid Status on response')
code_entry = OneLogin_Saml2_Utils.query(dom, '/samlp:Response/samlp:Status/samlp:StatusCode', status_entry[0])
- if len(code_entry) == 0:
- raise Exception('Missing Status Code on response')
+ if len(code_entry) != 1:
+ raise Exception('Missing valid Status Code on response')
code = code_entry[0].values()[0]
status['code'] = code
+ status['msg'] = ''
message_entry = OneLogin_Saml2_Utils.query(dom, '/samlp:Response/samlp:Status/samlp:StatusMessage', status_entry[0])
if len(message_entry) == 0:
subcode_entry = OneLogin_Saml2_Utils.query(dom, '/samlp:Response/samlp:Status/samlp:StatusCode/samlp:StatusCode', status_entry[0])
- if len(subcode_entry) > 0:
+ if len(subcode_entry) == 1:
status['msg'] = subcode_entry[0].values()[0]
- else:
- status['msg'] = ''
- else:
+ elif len(message_entry) == 1:
status['msg'] = message_entry[0].text
return status
diff --git a/tests/data/responses/invalids/duplicated_attributes.xml.base64 b/tests/data/responses/invalids/duplicated_attributes.xml.base64
new file mode 100644
index 00000000..a571b6d3
--- /dev/null
+++ b/tests/data/responses/invalids/duplicated_attributes.xml.base64
@@ -0,0 +1 @@
+PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeDQ0OTkyZWJiLTRiMzgtZTQzMi1kYjgyLTk5NTI0MTBkOWFhYiIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDMtMjFUMTM6NDI6MzFaIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzE5MWMwM2U2OGQ3MWQ5Nzk2ZjVlMDdlNjI2MmNhNGFkODgzYTc0YjEiPjxzYW1sOklzc3Vlcj5odHRwczovL3BpdGJ1bGsubm8taXAub3JnL3NpbXBsZXNhbWwvc2FtbDIvaWRwL21ldGFkYXRhLnBocDwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+DQogICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPg0KICA8ZHM6UmVmZXJlbmNlIFVSST0iI3BmeDQ0OTkyZWJiLTRiMzgtZTQzMi1kYjgyLTk5NTI0MTBkOWFhYiI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz48ZHM6RGlnZXN0VmFsdWU+Z3ZScnJneHBBZHlsSUEvMnNyRm1KZCtqaXM4PTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT5LZHA4VDhybndQY0JVb2hjcVBNMGVpTlhwTWgzbGMrZXBIVERIcUxFbk9Kcmd1NS9qaitpN0VhQW1nTzBSSlRraERFWTBWOEZuZVQ0dm92Y0FiZzlmYk04ZlRPMWxYODJ3SW1zRWRxMkwzU0U4NHFCdWFDbURWNVlvMDdDSGJRT1FqYWV0VGt0SnVvRjA4QWQ2bCs1aFJPL3BKeG1yRXlHKzRLaWhGWUJ1dWs9PC9kczpTaWduYXR1cmVWYWx1ZT4NCjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDgwYmFhZWY2LTI5MmItODc0Ny1jZmNhLWRlMWVlM2YxYTQxNSIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDMtMjFUMTM6NDI6MzFaIj48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPg0KICA8ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPg0KICAgIDxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjcnNhLXNoYTEiLz4NCiAgPGRzOlJlZmVyZW5jZSBVUkk9IiNwZng4MGJhYWVmNi0yOTJiLTg3NDctY2ZjYS1kZTFlZTNmMWE0MTUiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPmFSOU00ZXdOczN1K25KYVFDRDI2WjBBd0Q2TT08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+NGQ4WEo1bXBOaW1vQkhkenNXZi9aemxVTlE3SmlVeEl4K1B5TjRuM0EvbWExcGwvQ0FPSUtOUzZ0clR6STg5N1ZjbGxneFhhTTljUFZqOUhLYU9aRW4wSE5Qa2FWR3VjeVVPVzFUd2dWdnJVdkNNQXVRTzdRZ21aekd1SVhsblVKS3FpTDRZMThNT1M1VGpLaExoSG4xbGE4TEFucmRVVEJobUx5eGtjZjhVPTwvZHM6U2lnbmF0dXJlVmFsdWU+DQo8ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE+PGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlDZ1RDQ0Flb0NDUUNiT2xyV0RkWDdGVEFOQmdrcWhraUc5dzBCQVFVRkFEQ0JoREVMTUFrR0ExVUVCaE1DVGs4eEdEQVdCZ05WQkFnVEQwRnVaSEpsWVhNZ1UyOXNZbVZ5WnpFTU1Bb0dBMVVFQnhNRFJtOXZNUkF3RGdZRFZRUUtFd2RWVGtsT1JWUlVNUmd3RmdZRFZRUURFdzltWldsa1pTNWxjbXhoYm1jdWJtOHhJVEFmQmdrcWhraUc5dzBCQ1FFV0VtRnVaSEpsWVhOQWRXNXBibVYwZEM1dWJ6QWVGdzB3TnpBMk1UVXhNakF4TXpWYUZ3MHdOekE0TVRReE1qQXhNelZhTUlHRU1Rc3dDUVlEVlFRR0V3Sk9UekVZTUJZR0ExVUVDQk1QUVc1a2NtVmhjeUJUYjJ4aVpYSm5NUXd3Q2dZRFZRUUhFd05HYjI4eEVEQU9CZ05WQkFvVEIxVk9TVTVGVkZReEdEQVdCZ05WQkFNVEQyWmxhV1JsTG1WeWJHRnVaeTV1YnpFaE1COEdDU3FHU0liM0RRRUpBUllTWVc1a2NtVmhjMEIxYm1sdVpYUjBMbTV2TUlHZk1BMEdDU3FHU0liM0RRRUJBUVVBQTRHTkFEQ0JpUUtCZ1FEaXZiaFI3UDUxNngvUzNCcUt4dXBRZTBMT05vbGl1cGlCT2VzQ08zU0hiRHJsMytxOUliZm5mbUUwNHJOdU1jUHNJeEIxNjFUZERwSWVzTENuN2M4YVBISVNLT3RQbEFlVFpTbmI4UUF1N2FSalpxMytQYnJQNXVXM1RjZkNHUHRLVHl0SE9nZS9PbEpibzA3OGRWaFhRMTRkMUVEd1hKVzFyUlh1VXQ0QzhRSURBUUFCTUEwR0NTcUdTSWIzRFFFQkJRVUFBNEdCQUNEVmZwODZIT2JxWStlOEJVb1dROStWTVF4MUFTRG9oQmp3T3NnMld5a1VxUlhGK2RMZmNVSDlkV1I2M0N0WklLRkRiU3ROb21QblF6N25iSytvbnlnd0JzcFZFYm5IdVVpaFpxM1pVZG11bVFxQ3c0VXZzLzFVdnEzb3JPby9XSlZoVHl2TGdGVksyUWFyUTQvNjdPWmZIZDdSK1BPQlhob3BoU012MVpPbzwvZHM6WDUwOUNlcnRpZmljYXRlPjwvZHM6WDUwOURhdGE+PC9kczpLZXlJbmZvPjwvZHM6U2lnbmF0dXJlPjxzYW1sOlN1YmplY3Q+PHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9tZXRhZGF0YS5waHAiIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOm5hbWVpZC1mb3JtYXQ6dHJhbnNpZW50Ij5fMjEyNmRkMTliOGE5YTI4MjM4ZDg4ZmRjNzM4NWU2MDk5NTAwNGE3NzgyPC9zYW1sOk5hbWVJRD48c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbkRhdGEgTm90T25PckFmdGVyPSIyMDIzLTA5LTIyVDE5OjAyOjMxWiIgUmVjaXBpZW50PSJodHRwczovL3BpdGJ1bGsubm8taXAub3JnL25ld29uZWxvZ2luL2RlbW8xL2luZGV4LnBocD9hY3MiIEluUmVzcG9uc2VUbz0iT05FTE9HSU5fMTkxYzAzZTY4ZDcxZDk3OTZmNWUwN2U2MjYyY2E0YWQ4ODNhNzRiMSIvPjwvc2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uPjwvc2FtbDpTdWJqZWN0PjxzYW1sOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDE0LTAzLTIxVDEzOjQyOjAxWiIgTm90T25PckFmdGVyPSIyMDIzLTA5LTIyVDE5OjAyOjMxWiI+PHNhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48c2FtbDpBdWRpZW5jZT5odHRwczovL3BpdGJ1bGsubm8taXAub3JnL25ld29uZWxvZ2luL2RlbW8xL21ldGFkYXRhLnBocDwvc2FtbDpBdWRpZW5jZT48L3NhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48L3NhbWw6Q29uZGl0aW9ucz48c2FtbDpBdXRoblN0YXRlbWVudCBBdXRobkluc3RhbnQ9IjIwMTQtMDMtMjFUMTM6NDE6MDlaIiBTZXNzaW9uTm90T25PckFmdGVyPSIyMDE0LTAzLTIxVDIxOjQyOjMxWiIgU2Vzc2lvbkluZGV4PSJfZTY1NzhkNmFmOTdiOWY3ZjA2NzJkODUwZDI5ZGI0YWRkMWEyODZkYzI0Ij48c2FtbDpBdXRobkNvbnRleHQ+PHNhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFjOmNsYXNzZXM6UGFzc3dvcmQ8L3NhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+PC9zYW1sOkF1dGhuQ29udGV4dD48L3NhbWw6QXV0aG5TdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlU3RhdGVtZW50PjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJ1aWQiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnRlc3Q8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0idWlkIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj50ZXN0Mjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj50ZXN0QGV4YW1wbGUuY29tPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9ImNuIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj50ZXN0PC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9InNuIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj53YWEyPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9ImVkdVBlcnNvbkFmZmlsaWF0aW9uIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj51c2VyPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPmFkbWluPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD48L3NhbWw6QXNzZXJ0aW9uPjwvc2FtbHA6UmVzcG9uc2U+
\ No newline at end of file
diff --git a/tests/data/responses/invalids/empty_destination.xml.base64 b/tests/data/responses/invalids/empty_destination.xml.base64
new file mode 100644
index 00000000..352988ce
--- /dev/null
+++ b/tests/data/responses/invalids/empty_destination.xml.base64
@@ -0,0 +1 @@
+PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeDc2ZWY5MjAxLTY4OGItYzJkZC1mY2Q2LTQxMzEyNzE3ODk0OSIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIiBEZXN0aW5hdGlvbj0iIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciPjxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+DQogICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPg0KICA8ZHM6UmVmZXJlbmNlIFVSST0iI3BmeDc2ZWY5MjAxLTY4OGItYzJkZC1mY2Q2LTQxMzEyNzE3ODk0OSI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz48ZHM6RGlnZXN0VmFsdWU+TVJEd3dSTXZtalQ1VEhLUTBCNWRUNDVBNWhNPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT5wRFlrTFNKM2Z3TUQ0cnJNbWF4cFUwQkZVemZHQlVwNklURmovejNOTnFrQmdaTzdBMGIvQlFGbVBOQ202UE82NGdYNmVySGhhMVQ3aW5PTGRIY2crT0Q2Z2h2R0lpbGJzM1RjUkRwUmVTVkpZVWRiUS9jVk85aC9VdWNielBqZ3gyb3dpakk2aVh1dXhYcmpVeHEzYS9DbHcyTGJiVWJHMCtmQStud0ZuOVE9PC9kczpTaWduYXR1cmVWYWx1ZT4NCjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDk0Y2Q5YTMzLWQyOWMtMTMyMi1kYzMzLTFkOGU0ZDJiNTQzNSIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIj48c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPg0KICA8ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPg0KICAgIDxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjcnNhLXNoYTEiLz4NCiAgPGRzOlJlZmVyZW5jZSBVUkk9IiNwZng5NGNkOWEzMy1kMjljLTEzMjItZGMzMy0xZDhlNGQyYjU0MzUiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPnBYMkV3c1pVVUdCTGhYSTBVOVVMc3d0S3hDYz08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+TW5aVU04U0VmN3RVMlc1VGwvb0ZYTVBJYTZUVlcvUTczRmJUNUcxdW14eHZFRkM1UDlsR08reFVkdlBBTXdkTGc1aEN0R29QenB6amxCSnVFemhJU3VYblNZdkVCbllqdGJKVzcxcU9iM25WcTFjYVZtZXRhQjk4aUZzTDFvS0FWTVZ0Q0VST2E1SFpoT3VtQWJONU5qeHYvcUJlYW1lK0ExaStjV0FNaW13PTwvZHM6U2lnbmF0dXJlVmFsdWU+DQo8ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE+PGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlDZ1RDQ0Flb0NDUUNiT2xyV0RkWDdGVEFOQmdrcWhraUc5dzBCQVFVRkFEQ0JoREVMTUFrR0ExVUVCaE1DVGs4eEdEQVdCZ05WQkFnVEQwRnVaSEpsWVhNZ1UyOXNZbVZ5WnpFTU1Bb0dBMVVFQnhNRFJtOXZNUkF3RGdZRFZRUUtFd2RWVGtsT1JWUlVNUmd3RmdZRFZRUURFdzltWldsa1pTNWxjbXhoYm1jdWJtOHhJVEFmQmdrcWhraUc5dzBCQ1FFV0VtRnVaSEpsWVhOQWRXNXBibVYwZEM1dWJ6QWVGdzB3TnpBMk1UVXhNakF4TXpWYUZ3MHdOekE0TVRReE1qQXhNelZhTUlHRU1Rc3dDUVlEVlFRR0V3Sk9UekVZTUJZR0ExVUVDQk1QUVc1a2NtVmhjeUJUYjJ4aVpYSm5NUXd3Q2dZRFZRUUhFd05HYjI4eEVEQU9CZ05WQkFvVEIxVk9TVTVGVkZReEdEQVdCZ05WQkFNVEQyWmxhV1JsTG1WeWJHRnVaeTV1YnpFaE1COEdDU3FHU0liM0RRRUpBUllTWVc1a2NtVmhjMEIxYm1sdVpYUjBMbTV2TUlHZk1BMEdDU3FHU0liM0RRRUJBUVVBQTRHTkFEQ0JpUUtCZ1FEaXZiaFI3UDUxNngvUzNCcUt4dXBRZTBMT05vbGl1cGlCT2VzQ08zU0hiRHJsMytxOUliZm5mbUUwNHJOdU1jUHNJeEIxNjFUZERwSWVzTENuN2M4YVBISVNLT3RQbEFlVFpTbmI4UUF1N2FSalpxMytQYnJQNXVXM1RjZkNHUHRLVHl0SE9nZS9PbEpibzA3OGRWaFhRMTRkMUVEd1hKVzFyUlh1VXQ0QzhRSURBUUFCTUEwR0NTcUdTSWIzRFFFQkJRVUFBNEdCQUNEVmZwODZIT2JxWStlOEJVb1dROStWTVF4MUFTRG9oQmp3T3NnMld5a1VxUlhGK2RMZmNVSDlkV1I2M0N0WklLRkRiU3ROb21QblF6N25iSytvbnlnd0JzcFZFYm5IdVVpaFpxM1pVZG11bVFxQ3c0VXZzLzFVdnEzb3JPby9XSlZoVHl2TGdGVksyUWFyUTQvNjdPWmZIZDdSK1BPQlhob3BoU012MVpPbzwvZHM6WDUwOUNlcnRpZmljYXRlPjwvZHM6WDUwOURhdGE+PC9kczpLZXlJbmZvPjwvZHM6U2lnbmF0dXJlPjxzYW1sOlN1YmplY3Q+PHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvbWV0YWRhdGEucGhwIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+NDkyODgyNjE1YWNmMzFjODA5NmI2MjcyNDVkNzZhZTUzMDM2YzA5MDwvc2FtbDpOYW1lSUQ+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAyMy0wOC0yM1QwNjo1NzowMVoiIFJlY2lwaWVudD0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciLz48L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj48L3NhbWw6U3ViamVjdD48c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxNC0wMi0xOVQwMTozNjozMVoiIE5vdE9uT3JBZnRlcj0iMjAyMy0wOC0yM1QwNjo1NzowMVoiPjxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PHNhbWw6QXVkaWVuY2U+aHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvbWV0YWRhdGEucGhwPC9zYW1sOkF1ZGllbmNlPjwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPjwvc2FtbDpDb25kaXRpb25zPjxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxNC0wMi0xOVQwMTozNzowMVoiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwMTQtMDItMTlUMDk6Mzc6MDFaIiBTZXNzaW9uSW5kZXg9Il82MjczZDc3YjhjZGUwYzMzM2VjNzlkMjJhOWZhMDAwM2I5ZmUyZDc1Y2IiPjxzYW1sOkF1dGhuQ29udGV4dD48c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj48L3NhbWw6QXV0aG5Db250ZXh0Pjwvc2FtbDpBdXRoblN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlIE5hbWU9InVpZCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+c21hcnRpbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5zbWFydGluQHlhY28uZXM8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0iY24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPlNpeHRvMzwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJzbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+TWFydGluMjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJlZHVQZXJzb25BZmZpbGlhdGlvbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dXNlcjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5hZG1pbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PC9zYW1sOkFzc2VydGlvbj48L3NhbWxwOlJlc3BvbnNlPg==
\ No newline at end of file
diff --git a/tests/data/responses/invalids/empty_nameid.xml.base64 b/tests/data/responses/invalids/empty_nameid.xml.base64
new file mode 100644
index 00000000..72350d1f
--- /dev/null
+++ b/tests/data/responses/invalids/empty_nameid.xml.base64
@@ -0,0 +1 @@
+PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeDQ0MTM5Y2JkLWE2NTQtOWM1Mi00Njk3LTdjMDVkMzAyM2QyZiIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciPjxzYW1sOklzc3Vlcj5odHRwczovL3BpdGJ1bGsubm8taXAub3JnL3NpbXBsZXNhbWwvc2FtbDIvaWRwL21ldGFkYXRhLnBocDwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+DQogICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPg0KICA8ZHM6UmVmZXJlbmNlIFVSST0iI3BmeDQ0MTM5Y2JkLWE2NTQtOWM1Mi00Njk3LTdjMDVkMzAyM2QyZiI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz48ZHM6RGlnZXN0VmFsdWU+VEVFTFhxT0tmZVRqSFI5aUhPb2hrQWlCSDVVPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT51ZW1SeWgyQkcyTXBsbG5kWFNsV0tiaEgzZTRNQVd0VHNJYS9waWJndXZaRmhSTTVJNzUrRkFxYkl4UFVoWDlGYjlOTWRVRzdacWJJS2J0aitLZGxCdVlYaDdTdEIyQWMwY1VzamFQTHVLa2RTc0IzUzdESXFYRThmcEdNeHBSblNNZDZWc1RXM2RId3FYaTJiZklYblBDM0N0RjMwWUhXditwR081MFpCcjg9PC9kczpTaWduYXR1cmVWYWx1ZT4NCjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDVhMTU1NmIwLTE1NmYtZjNhNS04OGUyLTc1MzRkNjdiNjg0MyIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIj48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPg0KICA8ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPg0KICAgIDxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjcnNhLXNoYTEiLz4NCiAgPGRzOlJlZmVyZW5jZSBVUkk9IiNwZng1YTE1NTZiMC0xNTZmLWYzYTUtODhlMi03NTM0ZDY3YjY4NDMiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPnhpTEtIa05OcllPWTdWOFhkSjVET3pQNFp0ND08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+WHZDRURGdDBJM1VXWlMwN3JWa1VmNTA0Mjg3ZHJTbEI2bDBSdS9OTWMzZFlIT2E1V0NCNXZRanpGVURMSFZSQWlueWR0WXh3ejRTN1NKd081V3RKVFdTOStQNU9SMnpRTjRpYVpnclVGRm5xV0FDZW4rUTMzaXZVaFY0elVTcDU0cjVVdUxLNE96UnVhNmhlWUYrM0Y5TXZMK3VPV2hFZVc3NXZjODk0VXlVPTwvZHM6U2lnbmF0dXJlVmFsdWU+DQo8ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE+PGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlDZ1RDQ0Flb0NDUUNiT2xyV0RkWDdGVEFOQmdrcWhraUc5dzBCQVFVRkFEQ0JoREVMTUFrR0ExVUVCaE1DVGs4eEdEQVdCZ05WQkFnVEQwRnVaSEpsWVhNZ1UyOXNZbVZ5WnpFTU1Bb0dBMVVFQnhNRFJtOXZNUkF3RGdZRFZRUUtFd2RWVGtsT1JWUlVNUmd3RmdZRFZRUURFdzltWldsa1pTNWxjbXhoYm1jdWJtOHhJVEFmQmdrcWhraUc5dzBCQ1FFV0VtRnVaSEpsWVhOQWRXNXBibVYwZEM1dWJ6QWVGdzB3TnpBMk1UVXhNakF4TXpWYUZ3MHdOekE0TVRReE1qQXhNelZhTUlHRU1Rc3dDUVlEVlFRR0V3Sk9UekVZTUJZR0ExVUVDQk1QUVc1a2NtVmhjeUJUYjJ4aVpYSm5NUXd3Q2dZRFZRUUhFd05HYjI4eEVEQU9CZ05WQkFvVEIxVk9TVTVGVkZReEdEQVdCZ05WQkFNVEQyWmxhV1JsTG1WeWJHRnVaeTV1YnpFaE1COEdDU3FHU0liM0RRRUpBUllTWVc1a2NtVmhjMEIxYm1sdVpYUjBMbTV2TUlHZk1BMEdDU3FHU0liM0RRRUJBUVVBQTRHTkFEQ0JpUUtCZ1FEaXZiaFI3UDUxNngvUzNCcUt4dXBRZTBMT05vbGl1cGlCT2VzQ08zU0hiRHJsMytxOUliZm5mbUUwNHJOdU1jUHNJeEIxNjFUZERwSWVzTENuN2M4YVBISVNLT3RQbEFlVFpTbmI4UUF1N2FSalpxMytQYnJQNXVXM1RjZkNHUHRLVHl0SE9nZS9PbEpibzA3OGRWaFhRMTRkMUVEd1hKVzFyUlh1VXQ0QzhRSURBUUFCTUEwR0NTcUdTSWIzRFFFQkJRVUFBNEdCQUNEVmZwODZIT2JxWStlOEJVb1dROStWTVF4MUFTRG9oQmp3T3NnMld5a1VxUlhGK2RMZmNVSDlkV1I2M0N0WklLRkRiU3ROb21QblF6N25iSytvbnlnd0JzcFZFYm5IdVVpaFpxM1pVZG11bVFxQ3c0VXZzLzFVdnEzb3JPby9XSlZoVHl2TGdGVksyUWFyUTQvNjdPWmZIZDdSK1BPQlhob3BoU012MVpPbzwvZHM6WDUwOUNlcnRpZmljYXRlPjwvZHM6WDUwOURhdGE+PC9kczpLZXlJbmZvPjwvZHM6U2lnbmF0dXJlPjxzYW1sOlN1YmplY3Q+PHNhbWw6TmFtZUlEIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6MS4xOm5hbWVpZC1mb3JtYXQ6ZW1haWxBZGRyZXNzIi8+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAyMy0wOC0yM1QwNjo1NzowMVoiIFJlY2lwaWVudD0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciLz48L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj48L3NhbWw6U3ViamVjdD48c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxNC0wMi0xOVQwMTozNjozMVoiIE5vdE9uT3JBZnRlcj0iMjAyMy0wOC0yM1QwNjo1NzowMVoiPjxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PHNhbWw6QXVkaWVuY2U+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9tZXRhZGF0YS5waHA8L3NhbWw6QXVkaWVuY2U+PC9zYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PC9zYW1sOkNvbmRpdGlvbnM+PHNhbWw6QXV0aG5TdGF0ZW1lbnQgQXV0aG5JbnN0YW50PSIyMDE0LTAyLTE5VDAxOjM3OjAxWiIgU2Vzc2lvbk5vdE9uT3JBZnRlcj0iMjAxNC0wMi0xOVQwOTozNzowMVoiIFNlc3Npb25JbmRleD0iXzYyNzNkNzdiOGNkZTBjMzMzZWM3OWQyMmE5ZmEwMDAzYjlmZTJkNzVjYiI+PHNhbWw6QXV0aG5Db250ZXh0PjxzYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkPC9zYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPjwvc2FtbDpBdXRobkNvbnRleHQ+PC9zYW1sOkF1dGhuU3RhdGVtZW50PjxzYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGUgTmFtZT0idWlkIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5zbWFydGluPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9Im1haWwiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnNtYXJ0aW5AeWFjby5lczwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJjbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+U2l4dG8zPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9InNuIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5NYXJ0aW4yPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9ImVkdVBlcnNvbkFmZmlsaWF0aW9uIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj51c2VyPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPmFkbWluPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD48L3NhbWw6QXNzZXJ0aW9uPjwvc2FtbHA6UmVzcG9uc2U+
\ No newline at end of file
diff --git a/tests/data/responses/invalids/no_authnstatement.xml.base64 b/tests/data/responses/invalids/no_authnstatement.xml.base64
new file mode 100644
index 00000000..d116b7b5
--- /dev/null
+++ b/tests/data/responses/invalids/no_authnstatement.xml.base64
@@ -0,0 +1 @@
+PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGRmNWRiN2JiLTYwZDgtMWZhNi00OTBhLWFjMWMyZThjYWFhMiIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciPjxzYW1sOklzc3Vlcj5odHRwczovL3BpdGJ1bGsubm8taXAub3JnL3NpbXBsZXNhbWwvc2FtbDIvaWRwL21ldGFkYXRhLnBocDwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+DQogICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPg0KICA8ZHM6UmVmZXJlbmNlIFVSST0iI3BmeGRmNWRiN2JiLTYwZDgtMWZhNi00OTBhLWFjMWMyZThjYWFhMiI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz48ZHM6RGlnZXN0VmFsdWU+bGJTZmtFR0JsNmZEN0JBc1prU25wYmQyNGJFPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT56Y3YwSitsZ0V4R2tjSVVKYVp5ajdvZkFrY1VZc3dvckpiei9xdEo0WDBmSEtMYXB1eE0xYmlEbnJMTm5wUXhNSkJ3K092WG9sdWdHdVZBeEVyYmE5NTV2QlFtQTRCZXRZS0tKR09XcTkyMWpxKzVhdThtOWQzM2M1UTR6cDYzZld4UnRKV3AyVU05UnZ0aWd6enk2WWg0SE5yNVNkdUhzd1FJeFM2ZEQ2Lzg9PC9kczpTaWduYXR1cmVWYWx1ZT4NCjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeGI0ZWM5YzhhLTQ4ZWItZmRhMi03Zjc0LWZhMWExMDVhOTlmZSIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIj48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxzYW1sOlN1YmplY3Q+PHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9tZXRhZGF0YS5waHAiIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6MS4xOm5hbWVpZC1mb3JtYXQ6ZW1haWxBZGRyZXNzIj40OTI4ODI2MTVhY2YzMWM4MDk2YjYyNzI0NWQ3NmFlNTMwMzZjMDkwPC9zYW1sOk5hbWVJRD48c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbkRhdGEgTm90T25PckFmdGVyPSIyMDIzLTA4LTIzVDA2OjU3OjAxWiIgUmVjaXBpZW50PSJodHRwczovL3BpdGJ1bGsubm8taXAub3JnL25ld29uZWxvZ2luL2RlbW8xL2luZGV4LnBocD9hY3MiIEluUmVzcG9uc2VUbz0iT05FTE9HSU5fNWZlOWQ2ZTQ5OWIyZjA5MTMyMDZhYWIzZjcxOTE3MjkwNDliYjgwNyIvPjwvc2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uPjwvc2FtbDpTdWJqZWN0PjxzYW1sOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDE0LTAyLTE5VDAxOjM2OjMxWiIgTm90T25PckFmdGVyPSIyMDIzLTA4LTIzVDA2OjU3OjAxWiI+PHNhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48c2FtbDpBdWRpZW5jZT5odHRwczovL3BpdGJ1bGsubm8taXAub3JnL25ld29uZWxvZ2luL2RlbW8xL21ldGFkYXRhLnBocDwvc2FtbDpBdWRpZW5jZT48L3NhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48L3NhbWw6Q29uZGl0aW9ucz48c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlIE5hbWU9InVpZCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+c21hcnRpbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5zbWFydGluQHlhY28uZXM8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0iY24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPlNpeHRvMzwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJzbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+TWFydGluMjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJlZHVQZXJzb25BZmZpbGlhdGlvbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dXNlcjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5hZG1pbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PC9zYW1sOkFzc2VydGlvbj48L3NhbWxwOlJlc3BvbnNlPg==
\ No newline at end of file
diff --git a/tests/data/responses/invalids/no_conditions.xml.base64 b/tests/data/responses/invalids/no_conditions.xml.base64
new file mode 100644
index 00000000..4b73a83e
--- /dev/null
+++ b/tests/data/responses/invalids/no_conditions.xml.base64
@@ -0,0 +1 @@
+PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGQyMjJkZWI1LTZkMjktNWFiZC05NmM0LWFlOTk5ODZhYmVkNSIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciPjxzYW1sOklzc3Vlcj5odHRwczovL3BpdGJ1bGsubm8taXAub3JnL3NpbXBsZXNhbWwvc2FtbDIvaWRwL21ldGFkYXRhLnBocDwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+DQogICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPg0KICA8ZHM6UmVmZXJlbmNlIFVSST0iI3BmeGQyMjJkZWI1LTZkMjktNWFiZC05NmM0LWFlOTk5ODZhYmVkNSI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz48ZHM6RGlnZXN0VmFsdWU+Sm03Qm5JTEJ3V2h2TW1ZTjd4WG01dDR0ZEZVPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT5XRGw3K2RMclN4Ym95bTNWZzBXWTBrLzVDNWZxaDNNUWZPcXQraExDTXMwKzl3ekY4SHduWlJwLzRCMlJGOVBiUVAzc1d6VUY5QWNWeUErUFM4bU5aUnRzRzN4amFabE5BMWV3ZlQ3blFHZ1EvUkxLckhHeW9Bc3VaT0pLTDNqVjJiOGFSTE8rdSsrcmdoZUZSWm1wTkxVanBFTkdFZ3ZWc3ptcGN5aHFCd2c9PC9kczpTaWduYXR1cmVWYWx1ZT4NCjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeGI0ZWM5YzhhLTQ4ZWItZmRhMi03Zjc0LWZhMWExMDVhOTlmZSIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIj48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxzYW1sOlN1YmplY3Q+PHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9tZXRhZGF0YS5waHAiIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6MS4xOm5hbWVpZC1mb3JtYXQ6ZW1haWxBZGRyZXNzIj40OTI4ODI2MTVhY2YzMWM4MDk2YjYyNzI0NWQ3NmFlNTMwMzZjMDkwPC9zYW1sOk5hbWVJRD48c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbkRhdGEgTm90T25PckFmdGVyPSIyMDIzLTA4LTIzVDA2OjU3OjAxWiIgUmVjaXBpZW50PSJodHRwczovL3BpdGJ1bGsubm8taXAub3JnL25ld29uZWxvZ2luL2RlbW8xL2luZGV4LnBocD9hY3MiIEluUmVzcG9uc2VUbz0iT05FTE9HSU5fNWZlOWQ2ZTQ5OWIyZjA5MTMyMDZhYWIzZjcxOTE3MjkwNDliYjgwNyIvPjwvc2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uPjwvc2FtbDpTdWJqZWN0PjxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxNC0wMi0xOVQwMTozNzowMVoiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwMTQtMDItMTlUMDk6Mzc6MDFaIiBTZXNzaW9uSW5kZXg9Il82MjczZDc3YjhjZGUwYzMzM2VjNzlkMjJhOWZhMDAwM2I5ZmUyZDc1Y2IiPjxzYW1sOkF1dGhuQ29udGV4dD48c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj48L3NhbWw6QXV0aG5Db250ZXh0Pjwvc2FtbDpBdXRoblN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlIE5hbWU9InVpZCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+c21hcnRpbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5zbWFydGluQHlhY28uZXM8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0iY24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPlNpeHRvMzwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJzbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+TWFydGluMjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJlZHVQZXJzb25BZmZpbGlhdGlvbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dXNlcjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5hZG1pbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PC9zYW1sOkFzc2VydGlvbj48L3NhbWxwOlJlc3BvbnNlPg==
\ No newline at end of file
diff --git a/tests/data/responses/invalids/no_issuer_assertion.xml.base64 b/tests/data/responses/invalids/no_issuer_assertion.xml.base64
new file mode 100644
index 00000000..46094a6f
--- /dev/null
+++ b/tests/data/responses/invalids/no_issuer_assertion.xml.base64
@@ -0,0 +1 @@
+PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeDBlNmM5NjUzLTEwNjgtYzhjNS1iNzVjLWU2OTA1ZTE0M2Q0NCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciPjxzYW1sOklzc3Vlcj5odHRwczovL3BpdGJ1bGsubm8taXAub3JnL3NpbXBsZXNhbWwvc2FtbDIvaWRwL21ldGFkYXRhLnBocDwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+DQogICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPg0KICA8ZHM6UmVmZXJlbmNlIFVSST0iI3BmeDBlNmM5NjUzLTEwNjgtYzhjNS1iNzVjLWU2OTA1ZTE0M2Q0NCI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz48ZHM6RGlnZXN0VmFsdWU+KzhLSEl6dHh6SXNzMzNZZzlzRTVjTDlBRFpBPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT5yK21CNi9NU3pSS0VGNi9NZGY4M29QeE9ZelFWQ2IvUVIvNlVieG10cmVqbnRFRnN2ZFZSckhmMmd5TUUyZTBGd21ta3JQbEtzcHl2ZDhXbVN2ckV0T0pZaERLRWRYUThtUnRmZWgvY1M4M3pFYmRGSG9ubTd2YkJiU2VxSDBIN2g3S1UxSStqeEwyZVRpQWlubkpHeWhhVHNmaVAxNzdXZmlXVmQ4SHBOY289PC9kczpTaWduYXR1cmVWYWx1ZT4NCjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeGI0ZWM5YzhhLTQ4ZWItZmRhMi03Zjc0LWZhMWExMDVhOTlmZSIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIj48c2FtbDpTdWJqZWN0PjxzYW1sOk5hbWVJRCBTUE5hbWVRdWFsaWZpZXI9Imh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvbmV3b25lbG9naW4vZGVtbzEvbWV0YWRhdGEucGhwIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+NDkyODgyNjE1YWNmMzFjODA5NmI2MjcyNDVkNzZhZTUzMDM2YzA5MDwvc2FtbDpOYW1lSUQ+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAyMy0wOC0yM1QwNjo1NzowMVoiIFJlY2lwaWVudD0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciLz48L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj48L3NhbWw6U3ViamVjdD48c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxNC0wMi0xOVQwMTozNjozMVoiIE5vdE9uT3JBZnRlcj0iMjAyMy0wOC0yM1QwNjo1NzowMVoiPjxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PHNhbWw6QXVkaWVuY2U+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9tZXRhZGF0YS5waHA8L3NhbWw6QXVkaWVuY2U+PC9zYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PC9zYW1sOkNvbmRpdGlvbnM+PHNhbWw6QXV0aG5TdGF0ZW1lbnQgQXV0aG5JbnN0YW50PSIyMDE0LTAyLTE5VDAxOjM3OjAxWiIgU2Vzc2lvbk5vdE9uT3JBZnRlcj0iMjAxNC0wMi0xOVQwOTozNzowMVoiIFNlc3Npb25JbmRleD0iXzYyNzNkNzdiOGNkZTBjMzMzZWM3OWQyMmE5ZmEwMDAzYjlmZTJkNzVjYiI+PHNhbWw6QXV0aG5Db250ZXh0PjxzYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkPC9zYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPjwvc2FtbDpBdXRobkNvbnRleHQ+PC9zYW1sOkF1dGhuU3RhdGVtZW50PjxzYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGUgTmFtZT0idWlkIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5zbWFydGluPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9Im1haWwiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnNtYXJ0aW5AeWFjby5lczwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJjbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+U2l4dG8zPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9InNuIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5NYXJ0aW4yPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9ImVkdVBlcnNvbkFmZmlsaWF0aW9uIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj51c2VyPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPmFkbWluPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD48L3NhbWw6QXNzZXJ0aW9uPjwvc2FtbHA6UmVzcG9uc2U+
\ No newline at end of file
diff --git a/tests/data/responses/invalids/no_issuer_response.xml.base64 b/tests/data/responses/invalids/no_issuer_response.xml.base64
new file mode 100644
index 00000000..0e498d44
--- /dev/null
+++ b/tests/data/responses/invalids/no_issuer_response.xml.base64
@@ -0,0 +1 @@
+PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIElEPSJwZnhmMTA1MTkwNy0wZDZjLWI0NjctZjBiNC1kMDI4YTU4ZjNmNzIiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDE0LTAyLTE5VDAxOjM3OjAxWiIgRGVzdGluYXRpb249Imh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvbmV3b25lbG9naW4vZGVtbzEvaW5kZXgucGhwP2FjcyIgSW5SZXNwb25zZVRvPSJPTkVMT0dJTl81ZmU5ZDZlNDk5YjJmMDkxMzIwNmFhYjNmNzE5MTcyOTA0OWJiODA3Ij48c2FtbHA6U3RhdHVzPjxzYW1scDpTdGF0dXNDb2RlIFZhbHVlPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6c3RhdHVzOlN1Y2Nlc3MiLz48L3NhbWxwOlN0YXR1cz48c2FtbDpBc3NlcnRpb24geG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeG1sbnM6eHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hIiBJRD0icGZ4NGFhZGFlMTQtMmY5MC0xZDI1LWJlOTAtYjdjMzI3NzdkODU5IiBWZXJzaW9uPSIyLjAiIElzc3VlSW5zdGFudD0iMjAxNC0wMi0xOVQwMTozNzowMVoiPjxzYW1sOklzc3Vlcj5odHRwczovL3BpdGJ1bGsubm8taXAub3JnL3NpbXBsZXNhbWwvc2FtbDIvaWRwL21ldGFkYXRhLnBocDwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+DQogICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPg0KICA8ZHM6UmVmZXJlbmNlIFVSST0iI3BmeDRhYWRhZTE0LTJmOTAtMWQyNS1iZTkwLWI3YzMyNzc3ZDg1OSI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz48ZHM6RGlnZXN0VmFsdWU+T1R6Slg2cmNnUXdnM3dsOEZGMUZkUWFYY1QwPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT5GQlRWMXVGVk1WQ0NXelNvdTFxK3kvMzRZVVp1RnlLUzFyaktEREV0aHNVV0ZnVU10S3pQcU9VOFc2enN2MmdZaG0xQ09qd01yenFZUG5WTGViWmtQZ0VNYUlRZW9DR1M0M0pqYllzWk9sakgxZWo5Z3Z6SDM3NHBZMUd6UUx1QXllYmxlL3B4ZmZSMEY5NklYbnFjbjFySnJQM1puR0k1RGcxV3BpbVphWTQ9PC9kczpTaWduYXR1cmVWYWx1ZT4NCjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWw6U3ViamVjdD48c2FtbDpOYW1lSUQgU1BOYW1lUXVhbGlmaWVyPSJodHRwczovL3BpdGJ1bGsubm8taXAub3JnL25ld29uZWxvZ2luL2RlbW8xL21ldGFkYXRhLnBocCIgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoxLjE6bmFtZWlkLWZvcm1hdDplbWFpbEFkZHJlc3MiPjQ5Mjg4MjYxNWFjZjMxYzgwOTZiNjI3MjQ1ZDc2YWU1MzAzNmMwOTA8L3NhbWw6TmFtZUlEPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb24gTWV0aG9kPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y206YmVhcmVyIj48c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uRGF0YSBOb3RPbk9yQWZ0ZXI9IjIwMjMtMDgtMjNUMDY6NTc6MDFaIiBSZWNpcGllbnQ9Imh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvbmV3b25lbG9naW4vZGVtbzEvaW5kZXgucGhwP2FjcyIgSW5SZXNwb25zZVRvPSJPTkVMT0dJTl81ZmU5ZDZlNDk5YjJmMDkxMzIwNmFhYjNmNzE5MTcyOTA0OWJiODA3Ii8+PC9zYW1sOlN1YmplY3RDb25maXJtYXRpb24+PC9zYW1sOlN1YmplY3Q+PHNhbWw6Q29uZGl0aW9ucyBOb3RCZWZvcmU9IjIwMTQtMDItMTlUMDE6MzY6MzFaIiBOb3RPbk9yQWZ0ZXI9IjIwMjMtMDgtMjNUMDY6NTc6MDFaIj48c2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPjxzYW1sOkF1ZGllbmNlPmh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvbmV3b25lbG9naW4vZGVtbzEvbWV0YWRhdGEucGhwPC9zYW1sOkF1ZGllbmNlPjwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPjwvc2FtbDpDb25kaXRpb25zPjxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxNC0wMi0xOVQwMTozNzowMVoiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwMTQtMDItMTlUMDk6Mzc6MDFaIiBTZXNzaW9uSW5kZXg9Il82MjczZDc3YjhjZGUwYzMzM2VjNzlkMjJhOWZhMDAwM2I5ZmUyZDc1Y2IiPjxzYW1sOkF1dGhuQ29udGV4dD48c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj48L3NhbWw6QXV0aG5Db250ZXh0Pjwvc2FtbDpBdXRoblN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlIE5hbWU9InVpZCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+c21hcnRpbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5zbWFydGluQHlhY28uZXM8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0iY24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPlNpeHRvMzwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJzbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+TWFydGluMjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJlZHVQZXJzb25BZmZpbGlhdGlvbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dXNlcjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5hZG1pbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PC9zYW1sOkFzc2VydGlvbj48L3NhbWxwOlJlc3BvbnNlPg==
\ No newline at end of file
diff --git a/tests/data/responses/invalids/wrong_spnamequalifier.xml.base64 b/tests/data/responses/invalids/wrong_spnamequalifier.xml.base64
new file mode 100644
index 00000000..48e1fbff
--- /dev/null
+++ b/tests/data/responses/invalids/wrong_spnamequalifier.xml.base64
@@ -0,0 +1 @@
+PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeDEzNzFkZmU1LTdlMmYtMTdiNy1hODE1LTVmNWU5YTRiNjVkYSIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciPjxzYW1sOklzc3Vlcj5odHRwczovL3BpdGJ1bGsubm8taXAub3JnL3NpbXBsZXNhbWwvc2FtbDIvaWRwL21ldGFkYXRhLnBocDwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+DQogICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPg0KICA8ZHM6UmVmZXJlbmNlIFVSST0iI3BmeDEzNzFkZmU1LTdlMmYtMTdiNy1hODE1LTVmNWU5YTRiNjVkYSI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz48ZHM6RGlnZXN0VmFsdWU+c3lrMGNHOHAxOGpSMUtINThKNnR3Z0JNYlhzPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT4yaytTbmhjWWhSV0FLQWdLR2hyMVpZN1ZsSWtWSytsbEFKZncxVnllKzRkWFpZTGh0TkwrMUJ3bWRlVHlLY1BEYjNWSmZtNXRzRGFWRDNtTHVUd2E5L0EvUCt0ZnY0d0t6YVQrdmJvTDl0RVFnNFAwR3hmaWRzbkYrQUM0Z2lLb0VBRE5RbmlaamExR1hhM3VOdi85TEVYV1h4YmZaVGJPNmxLNnlhbGR6UkU9PC9kczpTaWduYXR1cmVWYWx1ZT4NCjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeGI0ZWM5YzhhLTQ4ZWItZmRhMi03Zjc0LWZhMWExMDVhOTlmZSIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIj48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxzYW1sOlN1YmplY3Q+PHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0id3Jvbmctc3AtZW50aXR5aWQiIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6MS4xOm5hbWVpZC1mb3JtYXQ6ZW1haWxBZGRyZXNzIj50ZXN0QGV4YW1wbGUuY29tPC9zYW1sOk5hbWVJRD48c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbkRhdGEgTm90T25PckFmdGVyPSIyMDIzLTA4LTIzVDA2OjU3OjAxWiIgUmVjaXBpZW50PSJodHRwczovL3BpdGJ1bGsubm8taXAub3JnL25ld29uZWxvZ2luL2RlbW8xL2luZGV4LnBocD9hY3MiIEluUmVzcG9uc2VUbz0iT05FTE9HSU5fNWZlOWQ2ZTQ5OWIyZjA5MTMyMDZhYWIzZjcxOTE3MjkwNDliYjgwNyIvPjwvc2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uPjwvc2FtbDpTdWJqZWN0PjxzYW1sOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDE0LTAyLTE5VDAxOjM2OjMxWiIgTm90T25PckFmdGVyPSIyMDIzLTA4LTIzVDA2OjU3OjAxWiI+PHNhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48c2FtbDpBdWRpZW5jZT5odHRwczovL3BpdGJ1bGsubm8taXAub3JnL25ld29uZWxvZ2luL2RlbW8xL21ldGFkYXRhLnBocDwvc2FtbDpBdWRpZW5jZT48L3NhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48L3NhbWw6Q29uZGl0aW9ucz48c2FtbDpBdXRoblN0YXRlbWVudCBBdXRobkluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIiBTZXNzaW9uTm90T25PckFmdGVyPSIyMDE0LTAyLTE5VDA5OjM3OjAxWiIgU2Vzc2lvbkluZGV4PSJfNjI3M2Q3N2I4Y2RlMGMzMzNlYzc5ZDIyYTlmYTAwMDNiOWZlMmQ3NWNiIj48c2FtbDpBdXRobkNvbnRleHQ+PHNhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFjOmNsYXNzZXM6UGFzc3dvcmQ8L3NhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+PC9zYW1sOkF1dGhuQ29udGV4dD48L3NhbWw6QXV0aG5TdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlU3RhdGVtZW50PjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJ1aWQiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnNtYXJ0aW48L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0ibWFpbCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dGVzdEBleGFtcGxlLmNvbTwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJjbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+U2l4dG8zPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9InNuIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5NYXJ0aW4yPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9ImVkdVBlcnNvbkFmZmlsaWF0aW9uIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj51c2VyPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPmFkbWluPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD48L3NhbWw6QXNzZXJ0aW9uPjwvc2FtbHA6UmVzcG9uc2U+
\ No newline at end of file
diff --git a/tests/data/responses/response_encrypted_nameid.xml.base64 b/tests/data/responses/response_encrypted_nameid.xml.base64
index 6b1c9856..d3bcbb6e 100644
--- a/tests/data/responses/response_encrypted_nameid.xml.base64
+++ b/tests/data/responses/response_encrypted_nameid.xml.base64
@@ -1 +1 @@
-PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGNlNWNjMjE0LTI1OGMtNjNkYy1iM2UxLWQ4YmI3ZGQ1ZWM1ZiIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDMtMDlUMTI6MjM6MzdaIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOX2JmMzcyYjlkNjdkMGM4OWQwY2YxYWYzZmY2MjVlYTdjMDUxYzk4ODUiPjxzYW1sOklzc3Vlcj5odHRwczovL3BpdGJ1bGsubm8taXAub3JnL3NpbXBsZXNhbWwvc2FtbDIvaWRwL21ldGFkYXRhLnBocDwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+DQogICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPg0KICA8ZHM6UmVmZXJlbmNlIFVSST0iI3BmeGNlNWNjMjE0LTI1OGMtNjNkYy1iM2UxLWQ4YmI3ZGQ1ZWM1ZiI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz48ZHM6RGlnZXN0VmFsdWU+aWptQ20xSDcxUE44TENNWWprbmx2YUVLMDB3PTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT5SeDNCODBrSUJnZm1UeU43Sy9HRXFNL0VmQWgwRVllV1NJN2RxdzBzdTRQUDdKclhjcng0N2ZwSjlLYWpRWlBTOU5CcVdjZlhJeVNVdGR5c1l4SUU3VzIvOHd0ZFozakVzbzZLRWlreDBTTlA0Z0RkUzNyNzVLMVNMb3NBZklVSmg2L0lUbll1Q2Y5UFl3RHNXVSt2WU9la2xnWC9xT3lkbm5sKzQ1QmpTd2M9PC9kczpTaWduYXR1cmVWYWx1ZT4NCjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDc1NmNjMjc5LWFmMjEtZDE5Mi1mMGVkLTEyNmRjZTYwMzBkNSIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDMtMDlUMTI6MjM6MzdaIj48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPg0KICA8ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPg0KICAgIDxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjcnNhLXNoYTEiLz4NCiAgPGRzOlJlZmVyZW5jZSBVUkk9IiNwZng3NTZjYzI3OS1hZjIxLWQxOTItZjBlZC0xMjZkY2U2MDMwZDUiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPks0MkdwcnJwREpLNmUvRlhpazBZYjl0cVpIVT08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+Z1pYSVkxWEw2UitNYzBzYWE5NzdSRzZEOXYzdUNkYUtXTUF5S0pUZUQ5UHIrL0NrWFNDbjJpbERXbUQyaldEVUVGNEZnZWtBMEt6STZQamRLR3VwRmVuZW96a2pFcHVmU0ozRkhpSXNxRnM5OGdONWZvZEEzRm16RjBLS2dScTRJaVRSd216UG5xT080eE8rQlhNQkoyTkFPVENaYnRyb3RsYnVBVlFUT3lzPTwvZHM6U2lnbmF0dXJlVmFsdWU+DQo8ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE+PGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlDZ1RDQ0Flb0NDUUNiT2xyV0RkWDdGVEFOQmdrcWhraUc5dzBCQVFVRkFEQ0JoREVMTUFrR0ExVUVCaE1DVGs4eEdEQVdCZ05WQkFnVEQwRnVaSEpsWVhNZ1UyOXNZbVZ5WnpFTU1Bb0dBMVVFQnhNRFJtOXZNUkF3RGdZRFZRUUtFd2RWVGtsT1JWUlVNUmd3RmdZRFZRUURFdzltWldsa1pTNWxjbXhoYm1jdWJtOHhJVEFmQmdrcWhraUc5dzBCQ1FFV0VtRnVaSEpsWVhOQWRXNXBibVYwZEM1dWJ6QWVGdzB3TnpBMk1UVXhNakF4TXpWYUZ3MHdOekE0TVRReE1qQXhNelZhTUlHRU1Rc3dDUVlEVlFRR0V3Sk9UekVZTUJZR0ExVUVDQk1QUVc1a2NtVmhjeUJUYjJ4aVpYSm5NUXd3Q2dZRFZRUUhFd05HYjI4eEVEQU9CZ05WQkFvVEIxVk9TVTVGVkZReEdEQVdCZ05WQkFNVEQyWmxhV1JsTG1WeWJHRnVaeTV1YnpFaE1COEdDU3FHU0liM0RRRUpBUllTWVc1a2NtVmhjMEIxYm1sdVpYUjBMbTV2TUlHZk1BMEdDU3FHU0liM0RRRUJBUVVBQTRHTkFEQ0JpUUtCZ1FEaXZiaFI3UDUxNngvUzNCcUt4dXBRZTBMT05vbGl1cGlCT2VzQ08zU0hiRHJsMytxOUliZm5mbUUwNHJOdU1jUHNJeEIxNjFUZERwSWVzTENuN2M4YVBISVNLT3RQbEFlVFpTbmI4UUF1N2FSalpxMytQYnJQNXVXM1RjZkNHUHRLVHl0SE9nZS9PbEpibzA3OGRWaFhRMTRkMUVEd1hKVzFyUlh1VXQ0QzhRSURBUUFCTUEwR0NTcUdTSWIzRFFFQkJRVUFBNEdCQUNEVmZwODZIT2JxWStlOEJVb1dROStWTVF4MUFTRG9oQmp3T3NnMld5a1VxUlhGK2RMZmNVSDlkV1I2M0N0WklLRkRiU3ROb21QblF6N25iSytvbnlnd0JzcFZFYm5IdVVpaFpxM1pVZG11bVFxQ3c0VXZzLzFVdnEzb3JPby9XSlZoVHl2TGdGVksyUWFyUTQvNjdPWmZIZDdSK1BPQlhob3BoU012MVpPbzwvZHM6WDUwOUNlcnRpZmljYXRlPjwvZHM6WDUwOURhdGE+PC9kczpLZXlJbmZvPjwvZHM6U2lnbmF0dXJlPjxzYW1sOlN1YmplY3Q+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAyMy0wOS0xMFQxNzo0MzozN1oiIFJlY2lwaWVudD0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOX2JmMzcyYjlkNjdkMGM4OWQwY2YxYWYzZmY2MjVlYTdjMDUxYzk4ODUiLz48L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj48c2FtbDpFbmNyeXB0ZWRJRD48eGVuYzpFbmNyeXB0ZWREYXRhIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyIgeG1sbnM6ZHNpZz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyIgVHlwZT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjRWxlbWVudCI+PHhlbmM6RW5jcnlwdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI3RyaXBsZWRlcy1jYmMiLz48ZHNpZzpLZXlJbmZvIHhtbG5zOmRzaWc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPjx4ZW5jOkVuY3J5cHRlZEtleT48eGVuYzpFbmNyeXB0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjcnNhLTFfNSIvPjx4ZW5jOkNpcGhlckRhdGE+PHhlbmM6Q2lwaGVyVmFsdWU+Mkx5Ry9wb1RrZUFsVHAvNmpEWUhGQ0JFbU9wUmZGUE5qZ2R5WDRWYTY5RDFkOXMycUIvcHB0UUg0UE8wZjl3cjVsbm9hWUhKME9CWUJJRjdnS1lEOVcvOEpVTUhLN1lNS2svandJcnhmWDNpVlRMQ1VMaTRoSWdRVUhRckR0cXg0eFNsNTJBckcrYzRxcDhNQk5ydTZ5T2lqSzB0NkRnemJGZURlNExxb0ZjPTwveGVuYzpDaXBoZXJWYWx1ZT48L3hlbmM6Q2lwaGVyRGF0YT48L3hlbmM6RW5jcnlwdGVkS2V5PjwvZHNpZzpLZXlJbmZvPg0KICAgPHhlbmM6Q2lwaGVyRGF0YT4NCiAgICAgIDx4ZW5jOkNpcGhlclZhbHVlPkJNWHRJZENXM2N6S0h5dk1tTGswMW9MWXNPY0NRcmFQblFzT3ZWN3pKL2Zzbnc3cG1pR2V5TlBsZFJQS2VRSVk1M2hJYTR4WUR5bmpUcHNHUERXZFpDUWo4US83R0R1TlMrSnArdUY2WWZSRzd1bEIyanBhU0xBVkRBRUt3eXVUVTBrZHhnTEw0L1BTZFVTQ1B1SURUMS94UURuZ2F6Q2I5RTNtY1ZsajZSODlxano0R1A5cVBlSC95WmZoV1EwYzRpNUs5NnZWOXFud09ZTUtSWjhHdFBLemNyK2RsbFhSSHlHQ09nOGtyUjd3Y0QxR1BvaHZmdVhvdm5xR3hMKzVPZExrMjVRbDJ4Y1Q1cUxNZnhGaEVxRDNteHVIVmN3WTwveGVuYzpDaXBoZXJWYWx1ZT4NCiAgIDwveGVuYzpDaXBoZXJEYXRhPg0KPC94ZW5jOkVuY3J5cHRlZERhdGE+PC9zYW1sOkVuY3J5cHRlZElEPjwvc2FtbDpTdWJqZWN0PjxzYW1sOkNvbmRpdGlvbnMgTm90QmVmb3JlPSIyMDE0LTAzLTA5VDEyOjIzOjA3WiIgTm90T25PckFmdGVyPSIyMDIzLTA5LTEwVDE3OjQzOjM3WiI+PHNhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48c2FtbDpBdWRpZW5jZT5odHRwczovL3BpdGJ1bGsubm8taXAub3JnL25ld29uZWxvZ2luL2RlbW8xL21ldGFkYXRhLnBocDwvc2FtbDpBdWRpZW5jZT48L3NhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48L3NhbWw6Q29uZGl0aW9ucz48c2FtbDpBdXRoblN0YXRlbWVudCBBdXRobkluc3RhbnQ9IjIwMTQtMDMtMDlUMTI6MjM6MzdaIiBTZXNzaW9uTm90T25PckFmdGVyPSIyMDE0LTAzLTA5VDIwOjIzOjM3WiIgU2Vzc2lvbkluZGV4PSJfOTQ0YmZjYWNiMGQ4MzJiMTJlNGJjZjc3NGUwMmJiZTVmNjQ1NWM2ODAzIj48c2FtbDpBdXRobkNvbnRleHQ+PHNhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+dXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFjOmNsYXNzZXM6UGFzc3dvcmQ8L3NhbWw6QXV0aG5Db250ZXh0Q2xhc3NSZWY+PC9zYW1sOkF1dGhuQ29udGV4dD48L3NhbWw6QXV0aG5TdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlU3RhdGVtZW50PjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJ1aWQiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPnRlc3Q8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0ibWFpbCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dGVzdEBleGFtcGxlLmNvbTwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJjbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dGVzdDwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJzbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+d2FhMjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJlZHVQZXJzb25BZmZpbGlhdGlvbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dXNlcjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5hZG1pbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PC9zYW1sOkFzc2VydGlvbj48L3NhbWxwOlJlc3BvbnNlPg==
+PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeDQyN2JmMGJiLTI3MmMtMGFkMC05MGYyLTVmN2ZkOWJhYzQ2OCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDMtMDlUMTI6MjM6MzdaIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOX2JmMzcyYjlkNjdkMGM4OWQwY2YxYWYzZmY2MjVlYTdjMDUxYzk4ODUiPjxzYW1sOklzc3Vlcj5odHRwczovL3BpdGJ1bGsubm8taXAub3JnL3NpbXBsZXNhbWwvc2FtbDIvaWRwL21ldGFkYXRhLnBocDwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+DQogICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPg0KICA8ZHM6UmVmZXJlbmNlIFVSST0iI3BmeDQyN2JmMGJiLTI3MmMtMGFkMC05MGYyLTVmN2ZkOWJhYzQ2OCI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz48ZHM6RGlnZXN0VmFsdWU+eHRBaE5WaUtVa3VmcUR1eUNHRDBVVE9SbVdBPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT5yL0E3SHhVVC9mZ1pXMVBGVkdHOXJTbW5sQU1QVjdnQm4zTjBVdFJ1V0VtTmlaQTVNUUVCSE9JeTA0dFFuSmRXYksrKzAxemh0TmdqRlhGeGduaE5VWFdKdVF4Rnp3SzdvaTRJbHc0amZjQ2tpSFJLVWxubGQ4U3Q1YVBGMUE5bFlWbWxtU0d5dUgzSnlrSjNnVjNaeVJRMjhDc0IvQnBtdi9mbmhleHBhYzQ9PC9kczpTaWduYXR1cmVWYWx1ZT4NCjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDczY2ZlZWM0LWZkOTMtMzE1OC05ZDI1LWVmZDgzYWM4MjQyYiIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDMtMDlUMTI6MjM6MzdaIj48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPg0KICA8ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPg0KICAgIDxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjcnNhLXNoYTEiLz4NCiAgPGRzOlJlZmVyZW5jZSBVUkk9IiNwZng3M2NmZWVjNC1mZDkzLTMxNTgtOWQyNS1lZmQ4M2FjODI0MmIiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPjJpdzJPZUpRSjA1ellQTkIvSUp3TXdETFRjdz08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+cFFiaUFReWEvKzVxbkhKWWlPQ0dFaUt5RHNSZThKdTlBUlJiVlFlUkd5NWF4VlU5aDYxV0tYSWcwcDI5NlFQMEJBem0rWmt5cXErTUdSVWE4OFNpaWFlbWRma3dRZnd1TS8rTk1FTkRqQmgrR2tob0RmcjN1ZDAzWjNDcHA3dDdTSldWVnF5NGZZVUtzNllaYmJRTG5PVjZPVVphb3R6eFBzUXVSVGlsWmVrPTwvZHM6U2lnbmF0dXJlVmFsdWU+DQo8ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE+PGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlDZ1RDQ0Flb0NDUUNiT2xyV0RkWDdGVEFOQmdrcWhraUc5dzBCQVFVRkFEQ0JoREVMTUFrR0ExVUVCaE1DVGs4eEdEQVdCZ05WQkFnVEQwRnVaSEpsWVhNZ1UyOXNZbVZ5WnpFTU1Bb0dBMVVFQnhNRFJtOXZNUkF3RGdZRFZRUUtFd2RWVGtsT1JWUlVNUmd3RmdZRFZRUURFdzltWldsa1pTNWxjbXhoYm1jdWJtOHhJVEFmQmdrcWhraUc5dzBCQ1FFV0VtRnVaSEpsWVhOQWRXNXBibVYwZEM1dWJ6QWVGdzB3TnpBMk1UVXhNakF4TXpWYUZ3MHdOekE0TVRReE1qQXhNelZhTUlHRU1Rc3dDUVlEVlFRR0V3Sk9UekVZTUJZR0ExVUVDQk1QUVc1a2NtVmhjeUJUYjJ4aVpYSm5NUXd3Q2dZRFZRUUhFd05HYjI4eEVEQU9CZ05WQkFvVEIxVk9TVTVGVkZReEdEQVdCZ05WQkFNVEQyWmxhV1JsTG1WeWJHRnVaeTV1YnpFaE1COEdDU3FHU0liM0RRRUpBUllTWVc1a2NtVmhjMEIxYm1sdVpYUjBMbTV2TUlHZk1BMEdDU3FHU0liM0RRRUJBUVVBQTRHTkFEQ0JpUUtCZ1FEaXZiaFI3UDUxNngvUzNCcUt4dXBRZTBMT05vbGl1cGlCT2VzQ08zU0hiRHJsMytxOUliZm5mbUUwNHJOdU1jUHNJeEIxNjFUZERwSWVzTENuN2M4YVBISVNLT3RQbEFlVFpTbmI4UUF1N2FSalpxMytQYnJQNXVXM1RjZkNHUHRLVHl0SE9nZS9PbEpibzA3OGRWaFhRMTRkMUVEd1hKVzFyUlh1VXQ0QzhRSURBUUFCTUEwR0NTcUdTSWIzRFFFQkJRVUFBNEdCQUNEVmZwODZIT2JxWStlOEJVb1dROStWTVF4MUFTRG9oQmp3T3NnMld5a1VxUlhGK2RMZmNVSDlkV1I2M0N0WklLRkRiU3ROb21QblF6N25iSytvbnlnd0JzcFZFYm5IdVVpaFpxM1pVZG11bVFxQ3c0VXZzLzFVdnEzb3JPby9XSlZoVHl2TGdGVksyUWFyUTQvNjdPWmZIZDdSK1BPQlhob3BoU012MVpPbzwvZHM6WDUwOUNlcnRpZmljYXRlPjwvZHM6WDUwOURhdGE+PC9kczpLZXlJbmZvPjwvZHM6U2lnbmF0dXJlPjxzYW1sOlN1YmplY3Q+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjA0My0wOS0xMFQxNzo0MzozN1oiIFJlY2lwaWVudD0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOX2JmMzcyYjlkNjdkMGM4OWQwY2YxYWYzZmY2MjVlYTdjMDUxYzk4ODUiLz48L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj48c2FtbDpFbmNyeXB0ZWRJRD48eGVuYzpFbmNyeXB0ZWREYXRhIHhtbG5zOnhlbmM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jIyIgeG1sbnM6ZHNpZz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyIgVHlwZT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjRWxlbWVudCI+PHhlbmM6RW5jcnlwdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI2FlczEyOC1jYmMiLz48ZHNpZzpLZXlJbmZvIHhtbG5zOmRzaWc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPjx4ZW5jOkVuY3J5cHRlZEtleT48eGVuYzpFbmNyeXB0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjcnNhLTFfNSIvPjx4ZW5jOkNpcGhlckRhdGE+PHhlbmM6Q2lwaGVyVmFsdWU+T0J0UmN6b2dyZWc1VmpOS29zL2JOaEEvMjZ1MnFiWko5ZzBvbWpsRGphWEp5N2FJRkJ4WTMyV3ZvcjY3T0VhNm5VV3dUcFpVT3FWR2NLQ1puMGtCWVZJUjFjSGJsTUdhRUNLdEthU25wdUR5UXZ4UDA1Ulp5cG5nUVJ3ZXdSZ0VaZkE5NmhCM2w1bmgzMFNqYmh4U09IbXk0Qi9lWEx1UHlOUWhiMEdadEFzPTwveGVuYzpDaXBoZXJWYWx1ZT48L3hlbmM6Q2lwaGVyRGF0YT48L3hlbmM6RW5jcnlwdGVkS2V5PjwvZHNpZzpLZXlJbmZvPg0KICAgPHhlbmM6Q2lwaGVyRGF0YT4NCiAgICAgIDx4ZW5jOkNpcGhlclZhbHVlPnNwZm8yNndVUlVCc3RnYzdBbG1PM25nem91OUFqUVU0NFhUOUt2ZWcvVTh6Q1FzNU43WXRyazZwREs2Tktic2gvQ1RMaFZKUy9kRFNwd3hzRy9QRDU0WlUrcmdKNGtndGt5YWduSzhydnBwNDJuYjc1YzBzUE1KK0M4dW42VHExeFBRRjlkcEJaVU1sZDI2WUJKQ0d6TG0vcmVDUlpiSk5iNEx3TmRjd3pQTnc4cnJsb1Z2SWhyQjlvWS80a3hwdExFemJmczVHcklxZXZNQnZjeWxpQVhBbFE1QitEZUdPWkhtODJnQmFBVHdxcXNuWkRsbmMyTUtiUWJyVXdLUTZ5NXdPcVByUmxXbFBJUnRLbzRZc2R3PT08L3hlbmM6Q2lwaGVyVmFsdWU+DQogICA8L3hlbmM6Q2lwaGVyRGF0YT4NCjwveGVuYzpFbmNyeXB0ZWREYXRhPjwvc2FtbDpFbmNyeXB0ZWRJRD48L3NhbWw6U3ViamVjdD48c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxNC0wMy0wOVQxMjoyMzowN1oiIE5vdE9uT3JBZnRlcj0iMjA0My0wOS0xMFQxNzo0MzozN1oiPjxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PHNhbWw6QXVkaWVuY2U+aHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvbWV0YWRhdGEucGhwPC9zYW1sOkF1ZGllbmNlPjwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPjwvc2FtbDpDb25kaXRpb25zPjxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxNC0wMy0wOVQxMjoyMzozN1oiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwNDMtMDMtMDlUMjA6MjM6MzdaIiBTZXNzaW9uSW5kZXg9Il85NDRiZmNhY2IwZDgzMmIxMmU0YmNmNzc0ZTAyYmJlNWY2NDU1YzY4MDMiPjxzYW1sOkF1dGhuQ29udGV4dD48c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj48L3NhbWw6QXV0aG5Db250ZXh0Pjwvc2FtbDpBdXRoblN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlIE5hbWU9InVpZCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dGVzdDwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj50ZXN0QGV4YW1wbGUuY29tPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9ImNuIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj50ZXN0PC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9InNuIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj53YWEyPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIE5hbWU9ImVkdVBlcnNvbkFmZmlsaWF0aW9uIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj51c2VyPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPmFkbWluPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD48L3NhbWw6QXNzZXJ0aW9uPjwvc2FtbHA6UmVzcG9uc2U+
\ No newline at end of file
diff --git a/tests/data/responses/valid_response.xml.base64 b/tests/data/responses/valid_response.xml.base64
index c727dc13..42ff8eb4 100644
--- a/tests/data/responses/valid_response.xml.base64
+++ b/tests/data/responses/valid_response.xml.base64
@@ -1 +1 @@
-PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIElEPSJwZngwNWYzY2UxMC0xNjE1LWYzZWEtYTk4OC02MGUzODBiMzI5OWYiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDE0LTAyLTE5VDAxOjM3OjAxWiIgRGVzdGluYXRpb249Imh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvbmV3b25lbG9naW4vZGVtbzEvaW5kZXgucGhwP2FjcyIgSW5SZXNwb25zZVRvPSJPTkVMT0dJTl81ZmU5ZDZlNDk5YjJmMDkxMzIwNmFhYjNmNzE5MTcyOTA0OWJiODA3Ij48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPgogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+CiAgICA8ZHM6U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3JzYS1zaGExIi8+CiAgPGRzOlJlZmVyZW5jZSBVUkk9IiNwZngwNWYzY2UxMC0xNjE1LWYzZWEtYTk4OC02MGUzODBiMzI5OWYiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPmpBZ290RjBKK1JLMS9LODd3MjRNTUMyK1pScz08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+cFhmM3Z3T1p2dGZtZjdNTWNPU0cwMzkyU213bnBJb0FqZ2dzVmErUlNJRE1Td0tTckwzcWw3SnlZQjVTaXZxL0xYODlUYXF5WTJ4MFBnTWl4YXY0bjFHMTFDM3NtbFJBTXJEZTZ2UnRJbUpVc2xTR2s5N3pQaHlvUStKNW9nUVBkNlZsTVR6OEtXemZxdE9QRGY1ZGwyWXlKcG9rZU9OVE0xemM0SkdNM0dBPTwvZHM6U2lnbmF0dXJlVmFsdWU+CjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeGI0ZWM5YzhhLTQ4ZWItZmRhMi03Zjc0LWZhMWExMDVhOTlmZSIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIj48c2FtbDpJc3N1ZXI+aHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9zaW1wbGVzYW1sL3NhbWwyL2lkcC9tZXRhZGF0YS5waHA8L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPgogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+CiAgICA8ZHM6U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3JzYS1zaGExIi8+CiAgPGRzOlJlZmVyZW5jZSBVUkk9IiNwZnhiNGVjOWM4YS00OGViLWZkYTItN2Y3NC1mYTFhMTA1YTk5ZmUiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPmhucFQ5Y3VlNnRCSzRJWk9qNTBlTW0zbUNnST08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+ckEwZkxaN0trbDNKd3NMS1hEd2FGY2c2TG5LSFJ6SVVyeDZtTlgxVG85ek1DdkNVRC9vUFlWYUVlTXRueFlZNDZmc21hTnZzS2l1Z2l4ZFZ6TEwzOEttQVk4dU1UZXpoR3Z6MlZlSFVvcjR1NGh4ZlFsc090MmZ1ekFFNkRicXZJVkswd2p4M1JZMDhYL3VPQ1I5dVBjcHk0QkJzUTN4UGw0UXZpcEpqU05NPTwvZHM6U2lnbmF0dXJlVmFsdWU+CjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWw6U3ViamVjdD48c2FtbDpOYW1lSUQgU1BOYW1lUXVhbGlmaWVyPSJodHRwczovL3BpdGJ1bGsubm8taXAub3JnL25ld29uZWxvZ2luL2RlbW8xL21ldGFkYXRhLnBocCIgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoxLjE6bmFtZWlkLWZvcm1hdDplbWFpbEFkZHJlc3MiPjQ5Mjg4MjYxNWFjZjMxYzgwOTZiNjI3MjQ1ZDc2YWU1MzAzNmMwOTA8L3NhbWw6TmFtZUlEPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb24gTWV0aG9kPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y206YmVhcmVyIj48c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uRGF0YSBOb3RPbk9yQWZ0ZXI9IjIwMjMtMDgtMjNUMDY6NTc6MDFaIiBSZWNpcGllbnQ9Imh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvbmV3b25lbG9naW4vZGVtbzEvaW5kZXgucGhwP2FjcyIgSW5SZXNwb25zZVRvPSJPTkVMT0dJTl81ZmU5ZDZlNDk5YjJmMDkxMzIwNmFhYjNmNzE5MTcyOTA0OWJiODA3Ii8+PC9zYW1sOlN1YmplY3RDb25maXJtYXRpb24+PC9zYW1sOlN1YmplY3Q+PHNhbWw6Q29uZGl0aW9ucyBOb3RCZWZvcmU9IjIwMTQtMDItMTlUMDE6MzY6MzFaIiBOb3RPbk9yQWZ0ZXI9IjIwMjMtMDgtMjNUMDY6NTc6MDFaIj48c2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPjxzYW1sOkF1ZGllbmNlPmh0dHBzOi8vcGl0YnVsay5uby1pcC5vcmcvbmV3b25lbG9naW4vZGVtbzEvbWV0YWRhdGEucGhwPC9zYW1sOkF1ZGllbmNlPjwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPjwvc2FtbDpDb25kaXRpb25zPjxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxNC0wMi0xOVQwMTozNzowMVoiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwMTQtMDItMTlUMDk6Mzc6MDFaIiBTZXNzaW9uSW5kZXg9Il82MjczZDc3YjhjZGUwYzMzM2VjNzlkMjJhOWZhMDAwM2I5ZmUyZDc1Y2IiPjxzYW1sOkF1dGhuQ29udGV4dD48c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj48L3NhbWw6QXV0aG5Db250ZXh0Pjwvc2FtbDpBdXRoblN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlIE5hbWU9InVpZCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+c21hcnRpbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5zbWFydGluQHlhY28uZXM8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0iY24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPlNpeHRvMzwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJzbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+TWFydGluMjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJlZHVQZXJzb25BZmZpbGlhdGlvbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dXNlcjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5hZG1pbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PC9zYW1sOkFzc2VydGlvbj48L3NhbWxwOlJlc3BvbnNlPg==
+PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGNjMzE1NjhiLWM0NmQtZmY3NS1iYTJlLTUzMDM0ODQ5ODBkYSIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciPjxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+DQogICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPg0KICA8ZHM6UmVmZXJlbmNlIFVSST0iI3BmeGNjMzE1NjhiLWM0NmQtZmY3NS1iYTJlLTUzMDM0ODQ5ODBkYSI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz48ZHM6RGlnZXN0VmFsdWU+cTZnVllaZUJmV21TelhzUUNEVUFYc2hyNjVRPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT5ES041ZWVwbm5CU3NvNVd3VHFwZ1lxQ2hsUXA1YXlVYUUrWlVVZ09CK2t5RDJEOUMxRjlSblVTK1VTa2hJQ2dDWVNTamtqQWE4OTZNNzgzdFFMd3dwcGZBSG1NMWpUcTRUVm1xKzlQOTZyQ29LUzUwR0FiZHVOUXFlSTl2T1EraTlXRWVkcFFFeWFsNEJNbS9pTkxHNE00Lzl5alRrVTU2OUdOOGZBOUJSb1E9PC9kczpTaWduYXR1cmVWYWx1ZT4NCjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDA4YzJhNmJiLTdlZTQtOGRjMi04ZmUyLWYwNTVlZWQ5M2RlNCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIj48c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPg0KICA8ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPg0KICAgIDxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjcnNhLXNoYTEiLz4NCiAgPGRzOlJlZmVyZW5jZSBVUkk9IiNwZngwOGMyYTZiYi03ZWU0LThkYzItOGZlMi1mMDU1ZWVkOTNkZTQiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPkVnQzhKSU1zL2VHaXdiSTBVc3AvWGRYUEtnOD08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+MW9JZFJ1bUJDaUNOdXVtYzVYNkhQYWI1L2xXZjZqR0dOR0dGOWxKRWxOc2lOYThpK3dSdkEveVF1YUFvMmp5bGNnV1pMWmZscjc2VnYrYVF0ZFA0LzNDR0djall5cUxWc0k0SS9iZjdXakk0dW1nMXl6aU5zNzMrelUzQThKK21LV013Q1J0eEZibm9BcnNyZzVFcVdOT2MrYkdWMEFsYnFCZTF4RllGcGVnPTwvZHM6U2lnbmF0dXJlVmFsdWU+DQo8ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE+PGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlDZ1RDQ0Flb0NDUUNiT2xyV0RkWDdGVEFOQmdrcWhraUc5dzBCQVFVRkFEQ0JoREVMTUFrR0ExVUVCaE1DVGs4eEdEQVdCZ05WQkFnVEQwRnVaSEpsWVhNZ1UyOXNZbVZ5WnpFTU1Bb0dBMVVFQnhNRFJtOXZNUkF3RGdZRFZRUUtFd2RWVGtsT1JWUlVNUmd3RmdZRFZRUURFdzltWldsa1pTNWxjbXhoYm1jdWJtOHhJVEFmQmdrcWhraUc5dzBCQ1FFV0VtRnVaSEpsWVhOQWRXNXBibVYwZEM1dWJ6QWVGdzB3TnpBMk1UVXhNakF4TXpWYUZ3MHdOekE0TVRReE1qQXhNelZhTUlHRU1Rc3dDUVlEVlFRR0V3Sk9UekVZTUJZR0ExVUVDQk1QUVc1a2NtVmhjeUJUYjJ4aVpYSm5NUXd3Q2dZRFZRUUhFd05HYjI4eEVEQU9CZ05WQkFvVEIxVk9TVTVGVkZReEdEQVdCZ05WQkFNVEQyWmxhV1JsTG1WeWJHRnVaeTV1YnpFaE1COEdDU3FHU0liM0RRRUpBUllTWVc1a2NtVmhjMEIxYm1sdVpYUjBMbTV2TUlHZk1BMEdDU3FHU0liM0RRRUJBUVVBQTRHTkFEQ0JpUUtCZ1FEaXZiaFI3UDUxNngvUzNCcUt4dXBRZTBMT05vbGl1cGlCT2VzQ08zU0hiRHJsMytxOUliZm5mbUUwNHJOdU1jUHNJeEIxNjFUZERwSWVzTENuN2M4YVBISVNLT3RQbEFlVFpTbmI4UUF1N2FSalpxMytQYnJQNXVXM1RjZkNHUHRLVHl0SE9nZS9PbEpibzA3OGRWaFhRMTRkMUVEd1hKVzFyUlh1VXQ0QzhRSURBUUFCTUEwR0NTcUdTSWIzRFFFQkJRVUFBNEdCQUNEVmZwODZIT2JxWStlOEJVb1dROStWTVF4MUFTRG9oQmp3T3NnMld5a1VxUlhGK2RMZmNVSDlkV1I2M0N0WklLRkRiU3ROb21QblF6N25iSytvbnlnd0JzcFZFYm5IdVVpaFpxM1pVZG11bVFxQ3c0VXZzLzFVdnEzb3JPby9XSlZoVHl2TGdGVksyUWFyUTQvNjdPWmZIZDdSK1BPQlhob3BoU012MVpPbzwvZHM6WDUwOUNlcnRpZmljYXRlPjwvZHM6WDUwOURhdGE+PC9kczpLZXlJbmZvPjwvZHM6U2lnbmF0dXJlPjxzYW1sOlN1YmplY3Q+PHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvbWV0YWRhdGEucGhwIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+NDkyODgyNjE1YWNmMzFjODA5NmI2MjcyNDVkNzZhZTUzMDM2YzA5MDwvc2FtbDpOYW1lSUQ+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAyMy0wOC0yM1QwNjo1NzowMVoiIFJlY2lwaWVudD0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciLz48L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj48L3NhbWw6U3ViamVjdD48c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxNC0wMi0xOVQwMTozNjozMVoiIE5vdE9uT3JBZnRlcj0iMjAyMy0wOC0yM1QwNjo1NzowMVoiPjxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PHNhbWw6QXVkaWVuY2U+aHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvbWV0YWRhdGEucGhwPC9zYW1sOkF1ZGllbmNlPjwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPjwvc2FtbDpDb25kaXRpb25zPjxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxNC0wMi0xOVQwMTozNzowMVoiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwMTQtMDItMTlUMDk6Mzc6MDFaIiBTZXNzaW9uSW5kZXg9Il82MjczZDc3YjhjZGUwYzMzM2VjNzlkMjJhOWZhMDAwM2I5ZmUyZDc1Y2IiPjxzYW1sOkF1dGhuQ29udGV4dD48c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj48L3NhbWw6QXV0aG5Db250ZXh0Pjwvc2FtbDpBdXRoblN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlIE5hbWU9InVpZCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+c21hcnRpbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5zbWFydGluQHlhY28uZXM8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0iY24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPlNpeHRvMzwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJzbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+TWFydGluMjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJlZHVQZXJzb25BZmZpbGlhdGlvbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dXNlcjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5hZG1pbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PC9zYW1sOkFzc2VydGlvbj48L3NhbWxwOlJlc3BvbnNlPg==
\ No newline at end of file
diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py
index 08602847..91a98278 100644
--- a/tests/src/OneLogin/saml2_tests/response_test.py
+++ b/tests/src/OneLogin/saml2_tests/response_test.py
@@ -114,6 +114,25 @@ def testReturnNameId(self):
except Exception as e:
self.assertIn('Not NameID found in the assertion of the Response', e.message)
+ json_settings['strict'] = True
+ settings = OneLogin_Saml2_Settings(json_settings)
+
+ xml_5 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'wrong_spnamequalifier.xml.base64'))
+ response_8 = OneLogin_Saml2_Response(settings, xml_5)
+ try:
+ response_8.get_nameid()
+ self.assertTrue(False)
+ except Exception as e:
+ self.assertIn('The SPNameQualifier value mistmatch the SP entityID value.', e.message)
+
+ xml_6 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'empty_nameid.xml.base64'))
+ response_9 = OneLogin_Saml2_Response(settings, xml_6)
+ try:
+ response_9.get_nameid()
+ self.assertTrue(False)
+ except Exception as e:
+ self.assertIn('An empty NameID value found', e.message)
+
def testGetNameIdData(self):
"""
Tests the get_nameid_data method of the OneLogin_Saml2_Response
@@ -135,7 +154,7 @@ def testGetNameIdData(self):
expected_nameid_data_2 = {
'Value': '2de11defd199f8d5bb63f9b7deb265ba5c675c10',
'Format': 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified',
- 'SPNameQualifier': 'https://pitbulk.no-ip.org/newonelogin/demo1/metadata.php'
+ 'SPNameQualifier': 'http://stuff.com/endpoints/metadata.php'
}
nameid_data_2 = response_2.get_nameid_data()
self.assertEqual(expected_nameid_data_2, nameid_data_2)
@@ -185,6 +204,25 @@ def testGetNameIdData(self):
except Exception as e:
self.assertIn('Not NameID found in the assertion of the Response', e.message)
+ json_settings['strict'] = True
+ settings = OneLogin_Saml2_Settings(json_settings)
+
+ xml_5 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'wrong_spnamequalifier.xml.base64'))
+ response_8 = OneLogin_Saml2_Response(settings, xml_5)
+ try:
+ response_8.get_nameid_data()
+ self.assertTrue(False)
+ except Exception as e:
+ self.assertIn('The SPNameQualifier value mistmatch the SP entityID value.', e.message)
+
+ xml_6 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'empty_nameid.xml.base64'))
+ response_9 = OneLogin_Saml2_Response(settings, xml_6)
+ try:
+ response_9.get_nameid_data()
+ self.assertTrue(False)
+ except Exception as e:
+ self.assertIn('An empty NameID value found', e.message)
+
def testCheckStatus(self):
"""
Tests the check_status method of the OneLogin_Saml2_Response
@@ -214,6 +252,45 @@ def testCheckStatus(self):
except Exception as e:
self.assertIn('The status code of the Response was not Success, was Responder -> something_is_wrong', e.message)
+ def testCheckOneCondition(self):
+ """
+ Tests the check_one_condition method of SamlResponse
+ """
+ settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
+ xml = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_conditions.xml.base64'))
+ response = OneLogin_Saml2_Response(settings, xml)
+ self.assertFalse(response.check_one_condition())
+
+ self.assertTrue(response.is_valid(self.get_request_data()))
+ settings.set_strict(True)
+ response = OneLogin_Saml2_Response(settings, xml)
+ self.assertFalse(response.is_valid(self.get_request_data()))
+ self.assertEquals('The Assertion must include a Conditions element', response.get_error())
+
+ xml_2 = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64'))
+ response_2 = OneLogin_Saml2_Response(settings, xml_2)
+ self.assertTrue(response_2.check_one_condition())
+
+
+ def testCheckOneAuthnStatement(self):
+ """
+ Tests the check_one_authnstatement method of SamlResponse
+ """
+ settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
+ xml = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_authnstatement.xml.base64'))
+ response = OneLogin_Saml2_Response(settings, xml)
+ self.assertFalse(response.check_one_authnstatement())
+
+ self.assertTrue(response.is_valid(self.get_request_data()))
+ settings.set_strict(True)
+ response = OneLogin_Saml2_Response(settings, xml)
+ self.assertFalse(response.is_valid(self.get_request_data()))
+ self.assertEquals('The Assertion must include an AuthnStatement element', response.get_error())
+
+ xml_2 = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64'))
+ response_2 = OneLogin_Saml2_Response(settings, xml_2)
+ self.assertTrue(response_2.check_one_authnstatement())
+
def testGetAudiences(self):
"""
Tests the get_audiences method of the OneLogin_Saml2_Response
@@ -237,9 +314,9 @@ def testQueryAssertions(self):
OneLogin_Saml2_Response using the get_issuers call
"""
settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
- xml = self.file_contents(join(self.data_path, 'responses', 'response1.xml.base64'))
+ xml = self.file_contents(join(self.data_path, 'responses', 'adfs_response.xml.base64'))
response = OneLogin_Saml2_Response(settings, xml)
- self.assertEqual(['https://app.onelogin.com/saml/metadata/13590'], response.get_issuers())
+ self.assertEqual(['http://login.example.com/issuer'], response.get_issuers())
xml_2 = self.file_contents(join(self.data_path, 'responses', 'valid_encrypted_assertion.xml.base64'))
response_2 = OneLogin_Saml2_Response(settings, xml_2)
@@ -270,9 +347,9 @@ def testGetIssuers(self):
Tests the get_issuers method of the OneLogin_Saml2_Response
"""
settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
- xml = self.file_contents(join(self.data_path, 'responses', 'response1.xml.base64'))
+ xml = self.file_contents(join(self.data_path, 'responses', 'adfs_response.xml.base64'))
response = OneLogin_Saml2_Response(settings, xml)
- self.assertEqual(['https://app.onelogin.com/saml/metadata/13590'], response.get_issuers())
+ self.assertEqual(['http://login.example.com/issuer'], response.get_issuers())
xml_2 = self.file_contents(join(self.data_path, 'responses', 'valid_encrypted_assertion.xml.base64'))
response_2 = OneLogin_Saml2_Response(settings, xml_2)
@@ -282,6 +359,20 @@ def testGetIssuers(self):
response_3 = OneLogin_Saml2_Response(settings, xml_3)
self.assertEqual(['http://idp.example.com/', 'https://pitbulk.no-ip.org/simplesaml/saml2/idp/metadata.php'], response_3.get_issuers())
+ xml_4 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_issuer_response.xml.base64'))
+ response_4 = OneLogin_Saml2_Response(settings, xml_4)
+ try:
+ issuers = response_4.get_issuers()
+ except Exception as e:
+ self.assertIn('Issuer of the Response not found or multiple.', e.message)
+
+ xml_5 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_issuer_assertion.xml.base64'))
+ response_5 = OneLogin_Saml2_Response(settings, xml_5)
+ try:
+ issuers = response_5.get_issuers()
+ except Exception as e:
+ self.assertIn('Issuer of the Assertion not found or multiple.', e.message)
+
def testGetSessionIndex(self):
"""
Tests the get_session_index method of the OneLogin_Saml2_Response
@@ -637,6 +728,20 @@ def testIsInValidEncAttrs(self):
except Exception as e:
self.assertEqual('There is an EncryptedAttribute in the Response and this SP not support them', e.message)
+ def testIsInValidDuplicatedAttrs(self):
+ """
+ Tests the getAttributes method of the OneLogin_Saml2_Response
+ Case duplicated Attrs
+ """
+ settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
+ xml = self.file_contents(join(self.data_path, 'responses', 'invalids', 'duplicated_attributes.xml.base64'))
+ response = OneLogin_Saml2_Response(settings, xml)
+ try:
+ attributes = response.get_attributes()
+ self.assertFalse(True)
+ except Exception as e:
+ self.assertEqual('Found an Attribute element with duplicated Name', e.message)
+
def testIsInValidDestination(self):
"""
Tests the is_valid method of the OneLogin_Saml2_Response class
@@ -653,18 +758,25 @@ def testIsInValidDestination(self):
self.assertFalse(response_2.is_valid(self.get_request_data()))
self.assertIn('The response was received at', response_2.get_error())
+ # Empty Destination
dom = parseString(b64decode(message))
dom.firstChild.setAttribute('Destination', '')
message_2 = b64encode(dom.toxml())
response_3 = OneLogin_Saml2_Response(settings, message_2)
self.assertFalse(response_3.is_valid(self.get_request_data()))
- self.assertIn('A valid SubjectConfirmation was not found on this Response', response_3.get_error())
+ self.assertIn('The response has an empty Destination value', response_3.get_error())
- dom.firstChild.removeAttribute('Destination')
- message_3 = b64encode(dom.toxml())
+ message_3 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'empty_destination.xml.base64'))
response_4 = OneLogin_Saml2_Response(settings, message_3)
self.assertFalse(response_4.is_valid(self.get_request_data()))
- self.assertIn('A valid SubjectConfirmation was not found on this Response', response_4.get_error())
+ self.assertEquals('The response has an empty Destination value', response_4.get_error())
+
+ # No Destination
+ dom.firstChild.removeAttribute('Destination')
+ message_4 = b64encode(dom.toxml())
+ response_5 = OneLogin_Saml2_Response(settings, message_4)
+ self.assertFalse(response_5.is_valid(self.get_request_data()))
+ self.assertIn('A valid SubjectConfirmation was not found on this Response', response_5.get_error())
def testIsInValidAudience(self):
"""
diff --git a/tests/src/OneLogin/saml2_tests/utils_test.py b/tests/src/OneLogin/saml2_tests/utils_test.py
index cb0a2a3d..61edf0d5 100644
--- a/tests/src/OneLogin/saml2_tests/utils_test.py
+++ b/tests/src/OneLogin/saml2_tests/utils_test.py
@@ -438,7 +438,7 @@ def testGetStatus(self):
status_inv = OneLogin_Saml2_Utils.get_status(dom_inv)
self.assertEqual(status_inv, 42)
except Exception as e:
- self.assertEqual('Missing Status on response', e.message)
+ self.assertEqual('Missing valid Status on response', e.message)
xml_inv2 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_status_code.xml.base64'))
xml_inv2 = b64decode(xml_inv2)
@@ -448,7 +448,7 @@ def testGetStatus(self):
status_inv2 = OneLogin_Saml2_Utils.get_status(dom_inv2)
self.assertEqual(status_inv2, 42)
except Exception as e:
- self.assertEqual('Missing Status Code on response', e.message)
+ self.assertEqual('Missing valid Status Code on response', e.message)
def testParseDuration(self):
"""
From 33095935f55e69fe3fa4874244b99fe6ca2d516b Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Fri, 7 Oct 2016 14:39:43 +0200
Subject: [PATCH 061/255] pep8 & pyflakes
---
tests/src/OneLogin/saml2_tests/response_test.py | 7 +++----
1 file changed, 3 insertions(+), 4 deletions(-)
diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py
index 91a98278..9ec754de 100644
--- a/tests/src/OneLogin/saml2_tests/response_test.py
+++ b/tests/src/OneLogin/saml2_tests/response_test.py
@@ -271,7 +271,6 @@ def testCheckOneCondition(self):
response_2 = OneLogin_Saml2_Response(settings, xml_2)
self.assertTrue(response_2.check_one_condition())
-
def testCheckOneAuthnStatement(self):
"""
Tests the check_one_authnstatement method of SamlResponse
@@ -362,14 +361,14 @@ def testGetIssuers(self):
xml_4 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_issuer_response.xml.base64'))
response_4 = OneLogin_Saml2_Response(settings, xml_4)
try:
- issuers = response_4.get_issuers()
+ response_4.get_issuers()
except Exception as e:
self.assertIn('Issuer of the Response not found or multiple.', e.message)
xml_5 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_issuer_assertion.xml.base64'))
response_5 = OneLogin_Saml2_Response(settings, xml_5)
try:
- issuers = response_5.get_issuers()
+ response_5.get_issuers()
except Exception as e:
self.assertIn('Issuer of the Assertion not found or multiple.', e.message)
@@ -737,7 +736,7 @@ def testIsInValidDuplicatedAttrs(self):
xml = self.file_contents(join(self.data_path, 'responses', 'invalids', 'duplicated_attributes.xml.base64'))
response = OneLogin_Saml2_Response(settings, xml)
try:
- attributes = response.get_attributes()
+ response.get_attributes()
self.assertFalse(True)
except Exception as e:
self.assertEqual('Found an Attribute element with duplicated Name', e.message)
From 80fab33ca6af711f3fbd90faff939b39f90a80e8 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Fri, 14 Oct 2016 01:15:15 +0200
Subject: [PATCH 062/255] Improve Signature validation process
---
src/onelogin/saml2/response.py | 64 ++++++++++++++-----
src/onelogin/saml2/utils.py | 17 +++--
.../saml2_tests/signed_response_test.py | 6 +-
3 files changed, 65 insertions(+), 22 deletions(-)
diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py
index b8aac5b3..8ea2f2f7 100644
--- a/src/onelogin/saml2/response.py
+++ b/src/onelogin/saml2/response.py
@@ -88,6 +88,9 @@ def is_valid(self, request_data, request_id=None):
signed_elements = self.process_signed_elements()
+ has_signed_response = '{%s}Response' % OneLogin_Saml2_Constants.NS_SAMLP in signed_elements
+ has_signed_assertion = '{%s}Assertion' % OneLogin_Saml2_Constants.NS_SAML in signed_elements
+
if self.__settings.is_strict():
no_valid_xml_msg = 'Invalid SAML Response. Not match the saml-schema-protocol-2.0.xsd'
res = OneLogin_Saml2_Utils.validate_xml(etree.tostring(self.document), 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active())
@@ -200,32 +203,26 @@ def is_valid(self, request_data, request_id=None):
if not any_subject_confirmation:
raise Exception('A valid SubjectConfirmation was not found on this Response')
- if security.get('wantAssertionsSigned', False) and ('{%s}Assertion' % OneLogin_Saml2_Constants.NS_SAML) not in signed_elements:
+ if security.get('wantAssertionsSigned', False) and not has_signed_assertion:
raise Exception('The Assertion of the Response is not signed and the SP require it')
- if security.get('wantMessagesSigned', False) and ('{%s}Response' % OneLogin_Saml2_Constants.NS_SAMLP) not in signed_elements:
+ if security.get('wantMessagesSigned', False) and not has_signed_response:
raise Exception('The Message of the Response is not signed and the SP require it')
- if len(signed_elements) > 0:
- if len(signed_elements) > 2:
- raise Exception('Too many Signatures found. SAML Response rejected')
+ if not signed_elements or (not has_signed_response and not has_signed_assertion):
+ raise Exception('No Signature found. SAML Response rejected')
+ else:
cert = idp_data.get('x509cert', None)
fingerprint = idp_data.get('certFingerprint', None)
fingerprintalg = idp_data.get('certFingerprintAlgorithm', None)
# If find a Signature on the Response, validates it checking the original response
- if '{%s}Response' % OneLogin_Saml2_Constants.NS_SAMLP in signed_elements:
- document_to_validate = self.document
- # Otherwise validates the assertion (decrypted assertion if was encrypted)
- else:
- if self.encrypted:
- document_to_validate = self.decrypted_document
- else:
- document_to_validate = self.document
- if not OneLogin_Saml2_Utils.validate_sign(document_to_validate, cert, fingerprint, fingerprintalg):
+ if has_signed_response and not OneLogin_Saml2_Utils.validate_sign(self.document, cert, fingerprint, fingerprintalg, xpath=OneLogin_Saml2_Utils.RESPONSE_SIGNATURE_XPATH):
+ raise Exception('Signature validation failed. SAML Response rejected')
+
+ document_check_assertion = self.decrypted_document if self.encrypted else self.document
+ if has_signed_assertion and not OneLogin_Saml2_Utils.validate_sign(document_check_assertion, cert, fingerprint, fingerprintalg, xpath=OneLogin_Saml2_Utils.ASSERTION_SIGNATURE_XPATH):
raise Exception('Signature validation failed. SAML Response rejected')
- else:
- raise Exception('No Signature found. SAML Response rejected')
return True
except Exception as err:
@@ -291,7 +288,7 @@ def get_issuers(self):
"""
issuers = []
- message_issuer_nodes = self.__query('/samlp:Response/saml:Issuer')
+ message_issuer_nodes = OneLogin_Saml2_Utils.query(self.document, '/samlp:Response/saml:Issuer')
if len(message_issuer_nodes) == 1:
issuers.append(message_issuer_nodes[0].text)
else:
@@ -486,8 +483,41 @@ def process_signed_elements(self):
verified_seis.append(sei)
signed_elements.append(signed_element)
+
+ if signed_elements:
+ if not self.validate_signed_elements(signed_elements):
+ raise Exception('Found an unexpected Signature Element. SAML Response rejected')
return signed_elements
+ def validate_signed_elements(self, signed_elements):
+ """
+ Verifies that the document has the expected signed nodes.
+ """
+ if len(signed_elements) > 2:
+ return False
+
+ response_tag = '{%s}Response' % OneLogin_Saml2_Constants.NS_SAMLP
+ assertion_tag = '{%s}Assertion' % OneLogin_Saml2_Constants.NS_SAML
+
+ if (response_tag in signed_elements and signed_elements.count(response_tag) > 1) or \
+ (assertion_tag in signed_elements and signed_elements.count(assertion_tag) > 1) or \
+ (response_tag not in signed_elements and assertion_tag not in signed_elements):
+ return False
+
+ # Check that the signed elements found here, are the ones that will be verified
+ # by OneLogin_Saml2_Utils.validate_sign
+ if response_tag in signed_elements:
+ expected_signature_nodes = OneLogin_Saml2_Utils.query(self.document, OneLogin_Saml2_Utils.RESPONSE_SIGNATURE_XPATH)
+ if len(expected_signature_nodes) != 1:
+ raise Exception('Unexpected number of Response signatures found. SAML Response rejected.')
+
+ if assertion_tag in signed_elements:
+ expected_signature_nodes = self.__query(OneLogin_Saml2_Utils.ASSERTION_SIGNATURE_XPATH)
+ if len(expected_signature_nodes) != 1:
+ raise Exception('Unexpected number of Assertion signatures found. SAML Response rejected.')
+
+ return True
+
def validate_timestamps(self):
"""
Verifies that the document is valid according to Conditions Element
diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py
index fbacf0ef..d7abf1cc 100644
--- a/src/onelogin/saml2/utils.py
+++ b/src/onelogin/saml2/utils.py
@@ -65,6 +65,9 @@ class OneLogin_Saml2_Utils(object):
"""
+ RESPONSE_SIGNATURE_XPATH = '/samlp:Response/ds:Signature'
+ ASSERTION_SIGNATURE_XPATH = '/samlp:Response/saml:Assertion/ds:Signature'
+
@staticmethod
def decode_base64_and_inflate(value):
"""
@@ -865,7 +868,7 @@ def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constant
return newdoc.saveXML(newdoc.firstChild)
@staticmethod
- def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', validatecert=False, debug=False):
+ def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', validatecert=False, debug=False, xpath=None):
"""
Validates a signature (Message or Assertion).
@@ -886,6 +889,9 @@ def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', valid
:param debug: Activate the xmlsec debug
:type: bool
+
+ :param xpath: The xpath of the signed element
+ :type: string
"""
try:
if xml is None or xml == '':
@@ -918,10 +924,13 @@ def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', valid
xmlsec.addIDs(elem, ["ID"])
- signature_nodes = OneLogin_Saml2_Utils.query(elem, '/samlp:Response/ds:Signature')
+ if xpath:
+ signature_nodes = OneLogin_Saml2_Utils.query(elem, xpath)
+ else:
+ signature_nodes = OneLogin_Saml2_Utils.query(elem, OneLogin_Saml2_Utils.RESPONSE_SIGNATURE_XPATH)
- if not len(signature_nodes) > 0:
- signature_nodes += OneLogin_Saml2_Utils.query(elem, '/samlp:Response/saml:Assertion/ds:Signature')
+ if len(signature_nodes) == 0:
+ signature_nodes = OneLogin_Saml2_Utils.query(elem, OneLogin_Saml2_Utils.ASSERTION_SIGNATURE_XPATH)
if len(signature_nodes) == 1:
signature_node = signature_nodes[0]
diff --git a/tests/src/OneLogin/saml2_tests/signed_response_test.py b/tests/src/OneLogin/saml2_tests/signed_response_test.py
index cdbba1fa..d630f00f 100644
--- a/tests/src/OneLogin/saml2_tests/signed_response_test.py
+++ b/tests/src/OneLogin/saml2_tests/signed_response_test.py
@@ -50,7 +50,11 @@ def testResponseAndAssertionSigned(self):
Tests the getNameId method of the OneLogin_Saml2_Response
Case valid signed response, signed assertion
"""
- settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
+ settings_info = self.loadSettingsJSON()
+ settings_info['idp']['entityId'] = "https://federate.example.net/saml/saml2/idp/metadata.php"
+ settings_info['sp']['entityId'] = "hello.com"
+
+ settings = OneLogin_Saml2_Settings(settings_info)
message = self.file_contents(join(self.data_path, 'responses', 'simple_saml_php.xml'))
response = OneLogin_Saml2_Response(settings, b64encode(message))
From 48addfdb3215933b8ae31532da0e214697477dec Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Fri, 14 Oct 2016 16:53:16 +0200
Subject: [PATCH 063/255] Release 2.2.0
---
README.md | 4 ++--
changelog.md | 21 +++++++++++++++++++++
setup.py | 6 +++---
3 files changed, 26 insertions(+), 5 deletions(-)
diff --git a/README.md b/README.md
index d4a43275..85ca137e 100644
--- a/README.md
+++ b/README.md
@@ -14,9 +14,9 @@ Python3: [python3-saml](https://github.com/onelogin/python3-saml).
#### Warning ####
-Update python-saml to 2.1.9, this version includes a security patch that contains extra validations that will prevent signature wrapping attacks.
+Update python-saml to 2.2.0, this version includes a security patch that contains extra validations that will prevent signature wrapping attacks.
-python-saml < v2.1.6 is vulnerable and allows signature wrapping!
+python-saml < v2.2.0 is vulnerable and allows signature wrapping!
#### Security Guidelines ####
diff --git a/changelog.md b/changelog.md
index 4e518024..b78c9f3c 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,5 +1,26 @@
# python-saml changelog
+### 2.2.0 (Oct 14, 2016)
+* Several security improvements:
+ * Conditions element required and unique.
+ * AuthnStatement element required and unique.
+ * SPNameQualifier must math the SP EntityID
+ * Reject saml:Attribute element with same “Name” attribute
+ * Reject empty nameID
+ * Require Issuer element. (Must match IdP EntityID).
+ * Destination value can't be blank (if present must match ACS URL).
+ * Check that the EncryptedAssertion element only contains 1 Assertion element.
+* Improve Signature validation process
+* [#149](https://github.com/onelogin/python-saml/pull/149) Work-around for xmlsec.initialize
+* [#151](https://github.com/onelogin/python-saml/pull/151) Fix flask demo error handling and improve documentation
+*
+* [#152](https://github.com/onelogin/python-saml/pull/152) Update LICENSE to include MIT rather than BSD license
+* [#155](https://github.com/onelogin/python-saml/pull/155) Fix typographical errors in docstring
+* Fix RequestedAttribute Issue
+* Fix __build_signature method. If relay_state is null not be part of the SignQuery
+* [#164](https://github.com/onelogin/python-saml/pull/164) Add support for non-ascii fields in settings
+
+
### 2.1.9 (Jun 27, 2016)
* Change the decrypt assertion process.
* Add 2 extra validations to prevent Signature wrapping attacks.
diff --git a/setup.py b/setup.py
index e157f95a..faa5708f 100644
--- a/setup.py
+++ b/setup.py
@@ -9,7 +9,7 @@
setup(
name='python-saml',
- version='2.1.9',
+ version='2.2.0',
description='Onelogin Python Toolkit. Add SAML support to your Python software using this library',
classifiers=[
'Development Status :: 4 - Beta',
@@ -22,9 +22,9 @@
author_email='support@onelogin.com',
license='MIT',
url='https://github.com/onelogin/python-saml',
- packages=['onelogin','onelogin/saml2'],
+ packages=['onelogin', 'onelogin/saml2'],
include_package_data=True,
- package_data = {
+ package_data={
'onelogin/saml2/schemas': ['*.xsd'],
},
package_dir={
From 54e5be1d8bb7f769c09e5de6b9f05ad6b6fe7e51 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Tue, 25 Oct 2016 14:07:57 +0200
Subject: [PATCH 064/255] Fix #167, related to #136
---
changelog.md | 1 -
src/onelogin/saml2/metadata.py | 2 +-
2 files changed, 1 insertion(+), 2 deletions(-)
diff --git a/changelog.md b/changelog.md
index b78c9f3c..40b03d4f 100644
--- a/changelog.md
+++ b/changelog.md
@@ -13,7 +13,6 @@
* Improve Signature validation process
* [#149](https://github.com/onelogin/python-saml/pull/149) Work-around for xmlsec.initialize
* [#151](https://github.com/onelogin/python-saml/pull/151) Fix flask demo error handling and improve documentation
-*
* [#152](https://github.com/onelogin/python-saml/pull/152) Update LICENSE to include MIT rather than BSD license
* [#155](https://github.com/onelogin/python-saml/pull/155) Fix typographical errors in docstring
* Fix RequestedAttribute Issue
diff --git a/src/onelogin/saml2/metadata.py b/src/onelogin/saml2/metadata.py
index 6ac03c27..fba83145 100644
--- a/src/onelogin/saml2/metadata.py
+++ b/src/onelogin/saml2/metadata.py
@@ -93,7 +93,7 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N
if 'friendlyName' in req_attribs.keys() and req_attribs['friendlyName']:
req_attr_nameformat_str = " FriendlyName=\"%s\"" % req_attribs['friendlyName']
if 'isRequired' in req_attribs.keys() and req_attribs['isRequired']:
- req_attr_isrequired_str = " isRequired=\"%s\"" % req_attribs['isRequired']
+ req_attr_isrequired_str = " isRequired=\"%s\"" % 'true' if req_attribs['isRequired'] else 'false'
if 'attributeValue' in req_attribs.keys() and req_attribs['attributeValue']:
if isinstance(req_attribs['attributeValue'], basestring):
From 4b1c5cc2445aa51e1319e754e7eac8806883aa39 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Thu, 17 Nov 2016 03:37:43 +0100
Subject: [PATCH 065/255] Update README.md
---
README.md | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/README.md b/README.md
index 85ca137e..395e688f 100644
--- a/README.md
+++ b/README.md
@@ -719,6 +719,15 @@ auth.process_slo(keep_local_session=keepLocalSession);
#### Initiate SLO ####
In order to send a Logout Request to the IdP:
+```python
+from onelogin.saml2.auth import OneLogin_Saml2_Auth
+
+req = prepare_request_for_toolkit(request)
+auth = OneLogin_Saml2_Auth(req) # Constructor of the SP, loads settings.json
+ # and advanced_settings.json
+
+auth.logout() # Method that builds and sends the LogoutRequest
+```
The Logout Request will be sent signed or unsigned based on the security info of the advanced_settings.json ('logoutRequestSigned').
From a088a3ea4ad5ce18ba5f026daba6ed7c7344a66e Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Tue, 29 Nov 2016 17:18:48 +0100
Subject: [PATCH 066/255] Extend get_last_request_xml and get_last_response_xml
to retrieve also Logout messages. Refactoring
---
README.md | 6 +--
src/onelogin/saml2/auth.py | 34 +++++++------
src/onelogin/saml2/authn_request.py | 4 +-
src/onelogin/saml2/logout_request.py | 9 ++++
src/onelogin/saml2/logout_response.py | 9 ++++
...> decrypted_valid_encrypted_assertion.xml} | 0
...y_decrypted_valid_encrypted_assertion.xml} | 0
tests/src/OneLogin/saml2_tests/auth_test.py | 6 ++-
.../saml2_tests/authn_request_test.py | 4 +-
.../saml2_tests/expensify_test.py.txt | 50 +++++++++++++++++++
.../src/OneLogin/saml2_tests/response_test.py | 2 +-
11 files changed, 100 insertions(+), 24 deletions(-)
rename tests/data/responses/{decrypted_valid_encrypted_assertion.xml.base64.xml => decrypted_valid_encrypted_assertion.xml} (100%)
rename tests/data/responses/{pretty_decrypted_valid_encrypted_assertion.xml.base64.xml => pretty_decrypted_valid_encrypted_assertion.xml} (100%)
create mode 100644 tests/src/OneLogin/saml2_tests/expensify_test.py.txt
diff --git a/README.md b/README.md
index 4e9f7a29..41bda4a8 100644
--- a/README.md
+++ b/README.md
@@ -827,13 +827,13 @@ Main class of OneLogin Python Toolkit
* ***get_last_error_reason*** Returns the reason of the last error
* ***get_sso_url*** Gets the SSO url.
* ***get_slo_url*** Gets the SLO url.
-* ***get_last_request_id*** The ID of the last Request SAML message generated.
+* ***get_last_request_id*** The ID of the last Request SAML message generated (AuthNRequest, LogoutRequest).
* ***build_request_signature*** Builds the Signature of the SAML Request.
* ***build_response_signature*** Builds the Signature of the SAML Response.
* ***get_settings*** Returns the settings info.
* ***set_strict*** Set the strict mode active/disable.
-* ***get_last_request_xml*** Returns the most recently-constructed XML request
-* ***get_last_response_xml*** Returns the most recently-decrypted XML response
+* ***get_last_request_xml*** Returns the most recently-constructed/processed XML SAML request (AuthNRequest, LogoutRequest)
+* ***get_last_response_xml*** Returns the most recently-constructed/processed XML SAML response (SAMLResponse, LogoutResponse). If the SAMLResponse was encrypted, by default tries to return the decrypted XML.
####OneLogin_Saml2_Auth - authn_request.py####
diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py
index e4693a7d..1d2d5cc6 100644
--- a/src/onelogin/saml2/auth.py
+++ b/src/onelogin/saml2/auth.py
@@ -58,8 +58,8 @@ def __init__(self, request_data, old_settings=None, custom_base_path=None):
self.__errors = []
self.__error_reason = None
self.__last_request_id = None
- self.__last_request_xml = None
- self.__last_response_xml = None
+ self.__last_request = None
+ self.__last_response = None
def get_settings(self):
"""
@@ -93,7 +93,7 @@ def process_response(self, request_id=None):
if 'post_data' in self.__request_data and 'SAMLResponse' in self.__request_data['post_data']:
# AuthnResponse -- HTTP_POST Binding
response = OneLogin_Saml2_Response(self.__settings, self.__request_data['post_data']['SAMLResponse'])
- self.__last_response_xml = response.get_xml_document()
+ self.__last_response = response.get_xml_document()
if response.is_valid(self.__request_data, request_id):
self.__attributes = response.get_attributes()
self.__nameid = response.get_nameid()
@@ -128,6 +128,7 @@ def process_slo(self, keep_local_session=False, request_id=None, delete_session_
if 'get_data' in self.__request_data and 'SAMLResponse' in self.__request_data['get_data']:
logout_response = OneLogin_Saml2_Logout_Response(self.__settings, self.__request_data['get_data']['SAMLResponse'])
+ self.__logout_response = logout_response.get_xml()
if not logout_response.is_valid(self.__request_data, request_id):
self.__errors.append('invalid_logout_response')
self.__error_reason = logout_response.get_error()
@@ -138,6 +139,7 @@ def process_slo(self, keep_local_session=False, request_id=None, delete_session_
elif 'get_data' in self.__request_data and 'SAMLRequest' in self.__request_data['get_data']:
logout_request = OneLogin_Saml2_Logout_Request(self.__settings, self.__request_data['get_data']['SAMLRequest'])
+ self.__last_request = logout_request.get_xml()
if not logout_request.is_valid(self.__request_data):
self.__errors.append('invalid_logout_request')
self.__error_reason = logout_request.get_error()
@@ -148,6 +150,7 @@ def process_slo(self, keep_local_session=False, request_id=None, delete_session_
in_response_to = logout_request.id
response_builder = OneLogin_Saml2_Logout_Response(self.__settings)
response_builder.build(in_response_to)
+ self.__logout_response = response_builder.get_xml()
logout_response = response_builder.get_response()
parameters = {'SAMLResponse': logout_response}
@@ -288,12 +291,11 @@ def login(self, return_to=None, force_authn=False, is_passive=False, set_nameid_
:rtype: string
"""
authn_request = OneLogin_Saml2_Authn_Request(self.__settings, force_authn, is_passive, set_nameid_policy)
-
+ self.__last_request = authn_request.get_xml()
self.__last_request_id = authn_request.get_id()
-
saml_request = authn_request.get_request()
+
parameters = {'SAMLRequest': saml_request}
- self.__last_request_xml = authn_request.get_request_as_xml()
if return_to is not None:
parameters['RelayState'] = return_to
else:
@@ -339,9 +341,8 @@ def logout(self, return_to=None, name_id=None, session_index=None, nq=None):
session_index=session_index,
nq=nq
)
-
+ self.__last_request = logout_request.get_xml()
self.__last_request_id = logout_request.id
-
saml_request = logout_request.get_request()
parameters = {'SAMLRequest': logout_request.get_request()}
@@ -455,15 +456,21 @@ def __build_signature(self, saml_data, relay_state, saml_type, sign_algorithm=On
signature = dsig_ctx.signBinary(str(msg), sign_algorithm_transform)
return b64encode(signature)
- def get_last_response_xml(self):
+ def get_last_response_xml(self, pretty_print_if_possible=False):
"""
- Retrieves the decrypted XML of the last SAML response
+ Retrieves the raw XML (decrypted) of the last SAML response,
+ or the last Logout Response generated or processed
:returns: SAML response XML
:rtype: string|None
"""
- if self.__last_response_xml:
- return etree.tostring(self.__last_response_xml, pretty_print=True)
+ response = None
+ if self.__last_response:
+ if isinstance(self.__last_response, basestring):
+ response = self.__last_response
+ else:
+ response = etree.tostring(self.__last_response, pretty_print=pretty_print_if_possible)
+ return response
def get_last_request_xml(self):
"""
@@ -472,5 +479,4 @@ def get_last_request_xml(self):
:returns: SAML request XML
:rtype: string|None
"""
- if self.__last_request_xml:
- return self.__last_request_xml
+ return self.__last_request or None
diff --git a/src/onelogin/saml2/authn_request.py b/src/onelogin/saml2/authn_request.py
index 3fa4d0f6..636a6888 100644
--- a/src/onelogin/saml2/authn_request.py
+++ b/src/onelogin/saml2/authn_request.py
@@ -150,9 +150,9 @@ def get_id(self):
"""
return self.__id
- def get_request_as_xml(self):
+ def get_xml(self):
"""
- Return the XML document that will be sent as part of the request
+ Returns the XML that will be sent as part of the request
:return: XML request body
:rtype: string
"""
diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py
index ab79be0d..e22b94cd 100644
--- a/src/onelogin/saml2/logout_request.py
+++ b/src/onelogin/saml2/logout_request.py
@@ -130,6 +130,15 @@ def get_request(self, deflate=True):
request = b64encode(self.__logout_request)
return request
+ def get_xml(self):
+ """
+ Returns the XML that will be sent as part of the request
+ or that was received at the SP
+ :return: XML request body
+ :rtype: string
+ """
+ return self.__logout_request
+
@staticmethod
def get_id(request):
"""
diff --git a/src/onelogin/saml2/logout_response.py b/src/onelogin/saml2/logout_response.py
index 5420ebbe..47c318d7 100644
--- a/src/onelogin/saml2/logout_response.py
+++ b/src/onelogin/saml2/logout_response.py
@@ -205,6 +205,15 @@ def get_response(self, deflate=True):
response = b64encode(self.__logout_response)
return response
+ def get_xml(self):
+ """
+ Returns the XML that will be sent as part of the response
+ or that was received at the SP
+ :return: XML response body
+ :rtype: string
+ """
+ return self.__logout_response
+
def get_error(self):
"""
After executing a validation process, if it fails this method returns the cause
diff --git a/tests/data/responses/decrypted_valid_encrypted_assertion.xml.base64.xml b/tests/data/responses/decrypted_valid_encrypted_assertion.xml
similarity index 100%
rename from tests/data/responses/decrypted_valid_encrypted_assertion.xml.base64.xml
rename to tests/data/responses/decrypted_valid_encrypted_assertion.xml
diff --git a/tests/data/responses/pretty_decrypted_valid_encrypted_assertion.xml.base64.xml b/tests/data/responses/pretty_decrypted_valid_encrypted_assertion.xml
similarity index 100%
rename from tests/data/responses/pretty_decrypted_valid_encrypted_assertion.xml.base64.xml
rename to tests/data/responses/pretty_decrypted_valid_encrypted_assertion.xml
diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py
index a83e10b8..0d59da9d 100644
--- a/tests/src/OneLogin/saml2_tests/auth_test.py
+++ b/tests/src/OneLogin/saml2_tests/auth_test.py
@@ -916,8 +916,10 @@ def testGetLastDecryptedResponse(self):
message_wrapper = {'post_data': {'SAMLResponse': message}}
auth = OneLogin_Saml2_Auth(message_wrapper, old_settings=settings)
auth.process_response()
- decrypted_response = self.file_contents(join(self.data_path, 'responses', 'pretty_decrypted_valid_encrypted_assertion.xml.base64.xml'))
- self.assertEqual(auth.get_last_response_xml(), decrypted_response)
+ decrypted_response = self.file_contents(join(self.data_path, 'responses', 'decrypted_valid_encrypted_assertion.xml'))
+ self.assertEqual(auth.get_last_response_xml(False), decrypted_response)
+ decrypted_response = self.file_contents(join(self.data_path, 'responses', 'pretty_decrypted_valid_encrypted_assertion.xml'))
+ self.assertEqual(auth.get_last_response_xml(True), decrypted_response)
def testGetLastSentRequest(self):
settings = self.loadSettingsJSON()
diff --git a/tests/src/OneLogin/saml2_tests/authn_request_test.py b/tests/src/OneLogin/saml2_tests/authn_request_test.py
index 4f298710..b26e2f82 100644
--- a/tests/src/OneLogin/saml2_tests/authn_request_test.py
+++ b/tests/src/OneLogin/saml2_tests/authn_request_test.py
@@ -80,7 +80,7 @@ def testGetRequestXML(self):
}
authn_request = OneLogin_Saml2_Authn_Request(settings)
- inflated = authn_request.get_request_as_xml()
+ inflated = authn_request.get_xml()
self.assertRegexpMatches(inflated, '^
Date: Wed, 30 Nov 2016 16:03:51 +0100
Subject: [PATCH 067/255] Add more test. Fix bug getting last response on
process_slo
---
src/onelogin/saml2/auth.py | 6 +-
.../pretty_signed_message_response.xml | 48 +++++++++++++++
tests/src/OneLogin/saml2_tests/auth_test.py | 61 +++++++++++++++++--
.../saml2_tests/authn_request_test.py | 12 ++--
.../saml2_tests/logout_request_test.py | 21 +++++++
.../saml2_tests/logout_response_test.py | 25 ++++++++
.../src/OneLogin/saml2_tests/response_test.py | 13 ++--
7 files changed, 169 insertions(+), 17 deletions(-)
create mode 100644 tests/data/responses/pretty_signed_message_response.xml
diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py
index 1d2d5cc6..ab8fb2af 100644
--- a/src/onelogin/saml2/auth.py
+++ b/src/onelogin/saml2/auth.py
@@ -128,7 +128,7 @@ def process_slo(self, keep_local_session=False, request_id=None, delete_session_
if 'get_data' in self.__request_data and 'SAMLResponse' in self.__request_data['get_data']:
logout_response = OneLogin_Saml2_Logout_Response(self.__settings, self.__request_data['get_data']['SAMLResponse'])
- self.__logout_response = logout_response.get_xml()
+ self.__last_response = logout_response.get_xml()
if not logout_response.is_valid(self.__request_data, request_id):
self.__errors.append('invalid_logout_response')
self.__error_reason = logout_response.get_error()
@@ -150,7 +150,7 @@ def process_slo(self, keep_local_session=False, request_id=None, delete_session_
in_response_to = logout_request.id
response_builder = OneLogin_Saml2_Logout_Response(self.__settings)
response_builder.build(in_response_to)
- self.__logout_response = response_builder.get_xml()
+ self.__last_response = response_builder.get_xml()
logout_response = response_builder.get_response()
parameters = {'SAMLResponse': logout_response}
@@ -465,7 +465,7 @@ def get_last_response_xml(self, pretty_print_if_possible=False):
:rtype: string|None
"""
response = None
- if self.__last_response:
+ if self.__last_response is not None:
if isinstance(self.__last_response, basestring):
response = self.__last_response
else:
diff --git a/tests/data/responses/pretty_signed_message_response.xml b/tests/data/responses/pretty_signed_message_response.xml
new file mode 100644
index 00000000..7dcb65ee
--- /dev/null
+++ b/tests/data/responses/pretty_signed_message_response.xml
@@ -0,0 +1,48 @@
+
+ https://pitbulk.no-ip.org/simplesaml/saml2/idp/metadata.php
+
+
+
+ 1dQFiYU0o2OF7c/RVV8Gpgb4u3I= wRgBXOq/FiLZc2mureTC/j6zY709OikJ5HeUSruHTdYjEg9aZy1RbxlKIYEIfXpnX7NBoKxfAMm+O0fsrqOjgcYxTVkqZjOr71qiXNbtwjeAkdYSpk5brsAcnfcPdv8QReYr3D7t5ZVCgYuvXQ+dNELKeag7e1ASOzVqOdp5Z9Y=
+MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo
+
+
+
+
+ https://pitbulk.no-ip.org/simplesaml/saml2/idp/metadata.php
+
+ _b98f98bb1ab512ced653b58baaff543448daed535d
+
+
+
+
+
+
+ https://pitbulk.no-ip.org/newonelogin/demo1/metadata.php
+
+
+
+
+ urn:oasis:names:tc:SAML:2.0:ac:classes:Password
+
+
+
+
+ test
+
+
+ test@example.com
+
+
+ test
+
+
+ waa2
+
+
+ user
+ admin
+
+
+
+
diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py
index 0d59da9d..1afaa32f 100644
--- a/tests/src/OneLogin/saml2_tests/auth_test.py
+++ b/tests/src/OneLogin/saml2_tests/auth_test.py
@@ -910,18 +910,26 @@ def testBuildResponseSignature(self):
except Exception as e:
self.assertIn("Trying to sign the SAMLResponse but can't load the SP private key", e.message)
- def testGetLastDecryptedResponse(self):
+ def testGetLastSAMLResponse(self):
settings = self.loadSettingsJSON()
+ message = self.file_contents(join(self.data_path, 'responses', 'signed_message_response.xml.base64'))
+ message_wrapper = {'post_data': {'SAMLResponse': message}}
+ auth = OneLogin_Saml2_Auth(message_wrapper, old_settings=settings)
+ auth.process_response()
+ expected_message = self.file_contents(join(self.data_path, 'responses', 'pretty_signed_message_response.xml'))
+ self.assertEqual(auth.get_last_response_xml(True), expected_message)
+
+ # with encrypted assertion
message = self.file_contents(join(self.data_path, 'responses', 'valid_encrypted_assertion.xml.base64'))
message_wrapper = {'post_data': {'SAMLResponse': message}}
auth = OneLogin_Saml2_Auth(message_wrapper, old_settings=settings)
auth.process_response()
decrypted_response = self.file_contents(join(self.data_path, 'responses', 'decrypted_valid_encrypted_assertion.xml'))
self.assertEqual(auth.get_last_response_xml(False), decrypted_response)
- decrypted_response = self.file_contents(join(self.data_path, 'responses', 'pretty_decrypted_valid_encrypted_assertion.xml'))
- self.assertEqual(auth.get_last_response_xml(True), decrypted_response)
+ pretty_decrypted_response = self.file_contents(join(self.data_path, 'responses', 'pretty_decrypted_valid_encrypted_assertion.xml'))
+ self.assertEqual(auth.get_last_response_xml(True), pretty_decrypted_response)
- def testGetLastSentRequest(self):
+ def testGetLastAuthnRequest(self):
settings = self.loadSettingsJSON()
auth = OneLogin_Saml2_Auth({'http_host': 'localhost', 'script_name': 'thing'}, old_settings=settings)
auth.login()
@@ -940,6 +948,51 @@ def testGetLastSentRequest(self):
)
self.assertIn(expectedFragment, auth.get_last_request_xml())
+ def testGetLastLogoutRequest(self):
+ settings = self.loadSettingsJSON()
+ auth = OneLogin_Saml2_Auth({'http_host': 'localhost', 'script_name': 'thing'}, old_settings=settings)
+ auth.logout()
+ expectedFragment = (
+ ' Destination="http://idp.example.com/SingleLogoutService.php">\n'
+ ' http://stuff.com/endpoints/metadata.php \n'
+ ' http://idp.example.com/ \n'
+ ' \n '
+ )
+ self.assertIn(expectedFragment, auth.get_last_request_xml())
+
+ request = self.file_contents(join(self.data_path, 'logout_requests', 'logout_request.xml'))
+ message = OneLogin_Saml2_Utils.deflate_and_base64_encode(request)
+ message_wrapper = {'get_data': {'SAMLRequest': message}}
+ auth = OneLogin_Saml2_Auth(message_wrapper, old_settings=settings)
+ auth.process_slo()
+ self.assertEqual(request, auth.get_last_request_xml())
+
+ def testGetLastLogoutResponse(self):
+ settings = self.loadSettingsJSON()
+ request = self.file_contents(join(self.data_path, 'logout_requests', 'logout_request.xml'))
+ message = OneLogin_Saml2_Utils.deflate_and_base64_encode(request)
+ message_wrapper = {'get_data': {'SAMLRequest': message}}
+ auth = OneLogin_Saml2_Auth(message_wrapper, old_settings=settings)
+ auth.process_slo()
+ expectedFragment = (
+ 'Destination="http://idp.example.com/SingleLogoutService.php"\n'
+ ' InResponseTo="ONELOGIN_21584ccdfaca36a145ae990442dcd96bfe60151e"\n>\n'
+ ' http://stuff.com/endpoints/metadata.php \n'
+ ' \n'
+ ' \n'
+ ' \n'
+ ''
+ )
+ self.assertIn(expectedFragment, auth.get_last_response_xml())
+
+ response = self.file_contents(join(self.data_path, 'logout_responses', 'logout_response.xml'))
+ message = OneLogin_Saml2_Utils.deflate_and_base64_encode(response)
+ message_wrapper = {'get_data': {'SAMLResponse': message}}
+ auth = OneLogin_Saml2_Auth(message_wrapper, old_settings=settings)
+ auth.process_slo()
+ self.assertEqual(response, auth.get_last_response_xml())
+
+
if __name__ == '__main__':
if is_running_under_teamcity():
runner = TeamcityTestRunner()
diff --git a/tests/src/OneLogin/saml2_tests/authn_request_test.py b/tests/src/OneLogin/saml2_tests/authn_request_test.py
index b26e2f82..6aee012d 100644
--- a/tests/src/OneLogin/saml2_tests/authn_request_test.py
+++ b/tests/src/OneLogin/saml2_tests/authn_request_test.py
@@ -64,25 +64,25 @@ def testCreateRequest(self):
self.assertRegexpMatches(inflated, '^\n'
+ ' http://stuff.com/endpoints/metadata.php \n'
+ ' http://idp.example.com/ \n'
+ ' \n '
+ )
+ self.assertIn(expectedFragment, logout_request_generated.get_xml())
+
+ logout_request_processed = OneLogin_Saml2_Logout_Request(settings, b64encode(request))
+ self.assertEqual(request, logout_request_processed.get_xml())
+
if __name__ == '__main__':
if is_running_under_teamcity():
diff --git a/tests/src/OneLogin/saml2_tests/logout_response_test.py b/tests/src/OneLogin/saml2_tests/logout_response_test.py
index d69c739c..4624e872 100644
--- a/tests/src/OneLogin/saml2_tests/logout_response_test.py
+++ b/tests/src/OneLogin/saml2_tests/logout_response_test.py
@@ -385,6 +385,31 @@ def testIsValid(self):
response_3 = OneLogin_Saml2_Logout_Response(settings, message_3)
self.assertTrue(response_3.is_valid(request_data))
+ def testGetXML(self):
+ """
+ Tests that we can get the logout response XML directly without
+ going through intermediate steps
+ """
+ response = self.file_contents(join(self.data_path, 'logout_responses', 'logout_response.xml'))
+ settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
+
+ logout_response_generated = OneLogin_Saml2_Logout_Response(settings)
+ logout_response_generated.build("InResponseValue")
+
+ expectedFragment = (
+ 'Destination="http://idp.example.com/SingleLogoutService.php"\n'
+ ' InResponseTo="InResponseValue"\n>\n'
+ ' http://stuff.com/endpoints/metadata.php \n'
+ ' \n'
+ ' \n'
+ ' \n'
+ ''
+ )
+ self.assertIn(expectedFragment, logout_response_generated.get_xml())
+
+ logout_response_processed = OneLogin_Saml2_Logout_Response(settings, OneLogin_Saml2_Utils.deflate_and_base64_encode(response))
+ self.assertEqual(response, logout_response_processed.get_xml())
+
if __name__ == '__main__':
if is_running_under_teamcity():
diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py
index 9b709c18..df781245 100644
--- a/tests/src/OneLogin/saml2_tests/response_test.py
+++ b/tests/src/OneLogin/saml2_tests/response_test.py
@@ -60,18 +60,23 @@ def testConstruct(self):
self.assertIsInstance(response_enc, OneLogin_Saml2_Response)
- def test_get_decrypted_xml(self):
+ def test_get_xml_document(self):
"""
Tests that we can retrieve the raw text of an encrypted XML response
without going through intermediate steps
"""
json_settings = self.loadSettingsJSON()
-
settings = OneLogin_Saml2_Settings(json_settings)
- xml = self.file_contents(join(self.data_path, 'responses', 'valid_encrypted_assertion.xml.base64'))
+
+ xml = self.file_contents(join(self.data_path, 'responses', 'signed_message_response.xml.base64'))
response = OneLogin_Saml2_Response(settings, xml)
+ prety_xml = self.file_contents(join(self.data_path, 'responses', 'pretty_signed_message_response.xml'))
+ self.assertEqual(etree.tostring(response.get_xml_document(), pretty_print=True), prety_xml)
+
+ xml_2 = self.file_contents(join(self.data_path, 'responses', 'valid_encrypted_assertion.xml.base64'))
+ response_2 = OneLogin_Saml2_Response(settings, xml_2)
decrypted = self.file_contents(join(self.data_path, 'responses', 'decrypted_valid_encrypted_assertion.xml'))
- self.assertEqual(etree.tostring(response.get_xml_document()), decrypted)
+ self.assertEqual(etree.tostring(response_2.get_xml_document()), decrypted)
def testReturnNameId(self):
"""
From 367d6dc64e3c2f9b325856e279d8788fd2ff3ac6 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Fri, 2 Dec 2016 01:27:47 +0100
Subject: [PATCH 068/255] Improved inResponse validation on Responses
---
src/onelogin/saml2/response.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py
index 50b80685..10908741 100644
--- a/src/onelogin/saml2/response.py
+++ b/src/onelogin/saml2/response.py
@@ -108,7 +108,7 @@ def is_valid(self, request_data, request_id=None):
# Check if the InResponseTo of the Response matchs the ID of the AuthNRequest (requestId) if provided
in_response_to = self.document.get('InResponseTo', None)
- if in_response_to and request_id:
+ if in_response_to is not None and request_id is not None:
if in_response_to != request_id:
raise Exception('The InResponseTo of the Response: %s, does not match the ID of the AuthNRequest sent by the SP: %s' % (in_response_to, request_id))
From 5cd77ca49f76d32fcd433816f643e643bb891818 Mon Sep 17 00:00:00 2001
From: Charles McBride
Date: Fri, 2 Dec 2016 11:03:45 -0600
Subject: [PATCH 069/255] Fix invalid JSON in readme.md
---
README.md | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/README.md b/README.md
index 41bda4a8..cc21560d 100644
--- a/README.md
+++ b/README.md
@@ -365,7 +365,7 @@ In addition to the required settings data (idp, sp), extra settings can be defin
// Indicates a requirement for the
// elements received by this SP to be encrypted.
- 'wantAssertionsEncrypted' => false,
+ "wantAssertionsEncrypted": false,
// Indicates a requirement for the NameID element on the SAMLResponse
// received by this SP to be present.
@@ -382,16 +382,16 @@ In addition to the required settings data (idp, sp), extra settings can be defin
// Set to false and no AuthContext will be sent in the AuthNRequest,
// Set true or don't present this parameter and you will get an AuthContext 'exact' 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport'
// Set an array with the possible auth context values: array ('urn:oasis:names:tc:SAML:2.0:ac:classes:Password', 'urn:oasis:names:tc:SAML:2.0:ac:classes:X509'),
- 'requestedAuthnContext': true,
+ "requestedAuthnContext": true,
// Allows the authn comparison parameter to be set, defaults to 'exact' if the setting is not present.
- 'requestedAuthnContextComparison': 'exact',
+ "requestedAuthnContextComparison": "exact",
// In some environment you will need to set how long the published metadata of the Service Provider gonna be valid.
// is possible to not set the 2 following parameters (or set to null) and default values will be set (2 days, 1 week)
// Provide the desired Timestamp, for example 2015-06-26T20:00:00Z
- 'metadataValidUntil': null,
+ "metadataValidUntil": null,
// Provide the desired duration, for example PT518400S (6 days)
- 'metadataCacheDuration': null,
+ "metadataCacheDuration": null,
// Algorithm that the toolkit will use on signing process. Options:
// 'http://www.w3.org/2000/09/xmldsig#rsa-sha1'
@@ -399,7 +399,7 @@ In addition to the required settings data (idp, sp), extra settings can be defin
// 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256'
// 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha384'
// 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha512'
- 'signatureAlgorithm' => 'http://www.w3.org/2000/09/xmldsig#rsa-sha1'
+ "signatureAlgorithm": "http://www.w3.org/2000/09/xmldsig#rsa-sha1"
},
// Contact information template, it is recommended to supply
From 963f297f63c01108dd0e2e71b509987a9c44b3cb Mon Sep 17 00:00:00 2001
From: Andrew Thal
Date: Mon, 12 Dec 2016 15:41:13 -0500
Subject: [PATCH 070/255] Fix attributeConsumingService serviceName format in
README.
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index cc21560d..bb1d9e5a 100644
--- a/README.md
+++ b/README.md
@@ -243,7 +243,7 @@ This is the settings.json file:
// attributeConsumingService. nameFormat, attributeValue and
// friendlyName can be omitted
"attributeConsumingService": {
- "ServiceName": "SP test",
+ "serviceName": "SP test",
"serviceDescription": "Test Service",
"requestedAttributes": [
{
From 0cd21ec9acda7018e513e8a0e28c3711b513a0a7 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Wed, 21 Dec 2016 10:22:13 +0100
Subject: [PATCH 071/255] Add option to raise exceptions on SAML message
validation
---
src/onelogin/saml2/logout_request.py | 7 +++++--
src/onelogin/saml2/logout_response.py | 8 ++++++--
src/onelogin/saml2/response.py | 4 +++-
.../OneLogin/saml2_tests/logout_request_test.py | 16 ++++++++++++++++
.../saml2_tests/logout_response_test.py | 17 +++++++++++++++++
tests/src/OneLogin/saml2_tests/response_test.py | 12 ++++++++++++
6 files changed, 59 insertions(+), 5 deletions(-)
diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py
index 131c69cc..99339e54 100644
--- a/src/onelogin/saml2/logout_request.py
+++ b/src/onelogin/saml2/logout_request.py
@@ -260,12 +260,13 @@ def get_session_indexes(request):
session_indexes.append(session_index_node.text)
return session_indexes
- def is_valid(self, request_data):
+ def is_valid(self, request_data, raise_exceptions=False):
"""
Checks if the Logout Request received is valid
:param request_data: Request Data
:type request_data: dict
-
+ :param raise_exceptions: Whether to return false on failure or raise an exception
+ :type raise_exceptions: Boolean
:return: If the Logout Request is or not valid
:rtype: boolean
"""
@@ -348,6 +349,8 @@ def is_valid(self, request_data):
debug = self.__settings.is_debug_active()
if debug:
print err.__str__()
+ if raise_exceptions:
+ raise err
return False
def get_error(self):
diff --git a/src/onelogin/saml2/logout_response.py b/src/onelogin/saml2/logout_response.py
index 3df89064..498272e0 100644
--- a/src/onelogin/saml2/logout_response.py
+++ b/src/onelogin/saml2/logout_response.py
@@ -68,11 +68,13 @@ def get_status(self):
status = entries[0].attrib['Value']
return status
- def is_valid(self, request_data, request_id=None):
+ def is_valid(self, request_data, request_id=None, raise_exceptions=False):
"""
Determines if the SAML LogoutResponse is valid
:param request_id: The ID of the LogoutRequest sent by this SP to the IdP
:type request_id: string
+ :param raise_exceptions: Whether to return false on failure or raise an exception
+ :type raise_exceptions: Boolean
:return: Returns if the SAML LogoutResponse is or not valid
:rtype: boolean
"""
@@ -89,7 +91,7 @@ def is_valid(self, request_data, request_id=None):
if self.__settings.is_strict():
res = OneLogin_Saml2_Utils.validate_xml(self.document, 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active())
if not isinstance(res, Document):
- raise Exception('Invalid SAML Logout Request. Not match the saml-schema-protocol-2.0.xsd')
+ raise Exception('Invalid SAML Logout Response. Not match the saml-schema-protocol-2.0.xsd')
security = self.__settings.get_security_data()
@@ -142,6 +144,8 @@ def is_valid(self, request_data, request_id=None):
debug = self.__settings.is_debug_active()
if debug:
print err.__str__()
+ if raise_exceptions:
+ raise err
return False
def __query(self, query):
diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py
index 620aff71..bd72ac74 100644
--- a/src/onelogin/saml2/response.py
+++ b/src/onelogin/saml2/response.py
@@ -51,7 +51,7 @@ def __init__(self, settings, response):
self.encrypted = True
self.decrypted_document = self.__decrypt_assertion(decrypted_document)
- def is_valid(self, request_data, request_id=None):
+ def is_valid(self, request_data, request_id=None, raise_exceptions=False):
"""
Validates the response object.
@@ -239,6 +239,8 @@ def is_valid(self, request_data, request_id=None):
debug = self.__settings.is_debug_active()
if debug:
print err.__str__()
+ if raise_exceptions:
+ raise err
return False
def check_status(self):
diff --git a/tests/src/OneLogin/saml2_tests/logout_request_test.py b/tests/src/OneLogin/saml2_tests/logout_request_test.py
index df115ee0..cbdfdb33 100644
--- a/tests/src/OneLogin/saml2_tests/logout_request_test.py
+++ b/tests/src/OneLogin/saml2_tests/logout_request_test.py
@@ -312,6 +312,22 @@ def testIsValid(self):
logout_request5 = OneLogin_Saml2_Logout_Request(settings, b64encode(request))
self.assertTrue(logout_request5.is_valid(request_data))
+ def testIsValidRaisesExceptionWhenRaisesArgumentIsTrue(self):
+ request = OneLogin_Saml2_Utils.deflate_and_base64_encode('invalid ')
+ request_data = {
+ 'http_host': 'example.com',
+ 'script_name': 'index.html'
+ }
+ settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
+ settings.set_strict(True)
+
+ logout_request = OneLogin_Saml2_Logout_Request(settings, request)
+
+ self.assertFalse(logout_request.is_valid(request_data))
+
+ with self.assertRaisesRegexp(Exception, "Invalid SAML Logout Request. Not match the saml-schema-protocol-2.0.xsd"):
+ logout_request.is_valid(request_data, raise_exceptions=True)
+
def testIsValidSign(self):
"""
Tests the is_valid method of the OneLogin_Saml2_LogoutRequest
diff --git a/tests/src/OneLogin/saml2_tests/logout_response_test.py b/tests/src/OneLogin/saml2_tests/logout_response_test.py
index 89d7bbf9..f0a1553b 100644
--- a/tests/src/OneLogin/saml2_tests/logout_response_test.py
+++ b/tests/src/OneLogin/saml2_tests/logout_response_test.py
@@ -228,6 +228,23 @@ def testIsInValidDestination(self):
response_4 = OneLogin_Saml2_Logout_Response(settings, message_4)
self.assertTrue(response_4.is_valid(request_data))
+ def testIsValidRaisesExceptionWhenRaisesArgumentIsTrue(self):
+ message = OneLogin_Saml2_Utils.deflate_and_base64_encode('invalid ')
+ request_data = {
+ 'http_host': 'example.com',
+ 'script_name': 'index.html',
+ 'get_data': {}
+ }
+ settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
+ settings.set_strict(True)
+
+ response = OneLogin_Saml2_Logout_Response(settings, message)
+
+ self.assertFalse(response.is_valid(request_data))
+
+ with self.assertRaisesRegexp(Exception, "Invalid SAML Logout Response. Not match the saml-schema-protocol-2.0.xsd"):
+ response.is_valid(request_data, raise_exceptions=True)
+
def testIsInValidSign(self):
"""
Tests the is_valid method of the OneLogin_Saml2_LogoutResponse
diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py
index 2f34efd8..2bd2aa7c 100644
--- a/tests/src/OneLogin/saml2_tests/response_test.py
+++ b/tests/src/OneLogin/saml2_tests/response_test.py
@@ -1227,6 +1227,18 @@ def testIsValidEnc(self):
response_7.is_valid(request_data)
self.assertEqual('No Signature found. SAML Response rejected', response_7.get_error())
+ def testIsValidRaisesExceptionWhenRaisesArgumentIsTrue(self):
+ message = b64encode('invalid ')
+ settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
+ settings.set_strict(True)
+
+ response = OneLogin_Saml2_Response(settings, message)
+
+ self.assertFalse(response.is_valid(self.get_request_data()))
+
+ with self.assertRaisesRegexp(Exception, "Unsupported SAML version"):
+ response.is_valid(self.get_request_data(), raise_exceptions=True)
+
def testIsValidSign(self):
"""
Tests the is_valid method of the OneLogin_Saml2_Response
From bc7bac63b72de309fd3361b6a83595da9fdf57c5 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Thu, 22 Dec 2016 09:06:17 +0100
Subject: [PATCH 072/255] Implement a more specific exception class for
handling some validation errors. Improve tests
---
src/onelogin/saml2/auth.py | 2 +-
src/onelogin/saml2/errors.py | 77 +++++-
src/onelogin/saml2/logout_request.py | 36 ++-
src/onelogin/saml2/logout_response.py | 36 ++-
src/onelogin/saml2/response.py | 230 ++++++++++++++----
src/onelogin/saml2/settings.py | 5 +-
src/onelogin/saml2/utils.py | 55 +++--
tests/src/OneLogin/saml2_tests/auth_test.py | 11 +-
.../saml2_tests/logout_request_test.py | 11 +-
.../saml2_tests/logout_response_test.py | 7 +-
.../src/OneLogin/saml2_tests/response_test.py | 36 +--
tests/src/OneLogin/saml2_tests/utils_test.py | 31 +--
12 files changed, 410 insertions(+), 127 deletions(-)
diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py
index ab8fb2af..096440eb 100644
--- a/src/onelogin/saml2/auth.py
+++ b/src/onelogin/saml2/auth.py
@@ -432,7 +432,7 @@ def __build_signature(self, saml_data, relay_state, saml_type, sign_algorithm=On
if not key:
raise OneLogin_Saml2_Error(
"Trying to sign the %s but can't load the SP private key" % saml_type,
- OneLogin_Saml2_Error.SP_CERTS_NOT_FOUND
+ OneLogin_Saml2_Error.PRIVATE_KEY_NOT_FOUND
)
dsig_ctx = xmlsec.DSigCtx()
diff --git a/src/onelogin/saml2/errors.py b/src/onelogin/saml2/errors.py
index 63d98744..5a859fc1 100644
--- a/src/onelogin/saml2/errors.py
+++ b/src/onelogin/saml2/errors.py
@@ -25,7 +25,7 @@ class OneLogin_Saml2_Error(Exception):
SETTINGS_INVALID_SYNTAX = 1
SETTINGS_INVALID = 2
METADATA_SP_INVALID = 3
- SP_CERTS_NOT_FOUND = 4
+ CERT_NOT_FOUND = 4
REDIRECT_INVALID_URL = 5
PUBLIC_CERT_FILE_NOT_FOUND = 6
PRIVATE_KEY_FILE_NOT_FOUND = 7
@@ -34,6 +34,8 @@ class OneLogin_Saml2_Error(Exception):
SAML_LOGOUTREQUEST_INVALID = 10
SAML_LOGOUTRESPONSE_INVALID = 11
SAML_SINGLE_LOGOUT_NOT_SUPPORTED = 12
+ PRIVATE_KEY_NOT_FOUND = 13
+ UNSUPPORTED_SETTINGS_OBJECT = 14
def __init__(self, message, code=0, errors=None):
"""
@@ -51,3 +53,76 @@ def __init__(self, message, code=0, errors=None):
Exception.__init__(self, message)
self.code = code
+
+
+class OneLogin_Saml2_ValidationError(Exception):
+ """
+
+ This class implements another custom Exception handler, related
+ to exceptions that happens during validation process.
+ Defines custom error codes .
+
+ """
+
+ # Validation Errors
+ UNSUPPORTED_SAML_VERSION = 0
+ MISSING_ID = 1
+ WRONG_NUMBER_OF_ASSERTIONS = 2
+ MISSING_STATUS = 3
+ MISSING_STATUS_CODE = 4
+ STATUS_CODE_IS_NOT_SUCCESS = 5
+ WRONG_SIGNED_ELEMENT = 6
+ ID_NOT_FOUND_IN_SIGNED_ELEMENT = 7
+ DUPLICATED_ID_IN_SIGNED_ELEMENTS = 8
+ INVALID_SIGNED_ELEMENT = 9
+ DUPLICATED_REFERENCE_IN_SIGNED_ELEMENTS = 10
+ UNEXPECTED_SIGNED_ELEMENTS = 11
+ WRONG_NUMBER_OF_SIGNATURES_IN_RESPONSE = 12
+ WRONG_NUMBER_OF_SIGNATURES_IN_ASSERTION = 13
+ INVALID_XML_FORMAT = 14
+ WRONG_INRESPONSETO = 15
+ NO_ENCRYPTED_ASSERTION = 16
+ NO_ENCRYPTED_NAMEID = 17
+ MISSING_CONDITIONS = 18
+ ASSERTION_TOO_EARLY = 19
+ ASSERTION_EXPIRED = 20
+ WRONG_NUMBER_OF_AUTHSTATEMENTS = 21
+ NO_ATTRIBUTESTATEMENT = 22
+ ENCRYPTED_ATTRIBUTES = 23
+ WRONG_DESTINATION = 24
+ EMPTY_DESTINATION = 25
+ WRONG_AUDIENCE = 26
+ ISSUER_NOT_FOUND_IN_RESPONSE = 27
+ ISSUER_NOT_FOUND_IN_ASSERTION = 28
+ WRONG_ISSUER = 29
+ SESSION_EXPIRED = 30
+ WRONG_SUBJECTCONFIRMATION = 31
+ NO_SIGNED_RESPONSE = 32
+ NO_SIGNED_ASSERTION = 33
+ NO_SIGNATURE_FOUND = 34
+ KEYINFO_NOT_FOUND_IN_ENCRYPTED_DATA = 35
+ CHILDREN_NODE_NOT_FOIND_IN_KEYINFO = 36
+ UNSUPPORTED_RETRIEVAL_METHOD = 37
+ NO_NAMEID = 38
+ EMPTY_NAMEID = 39
+ SP_NAME_QUALIFIER_NAME_MISMATCH = 40
+ DUPLICATED_ATTRIBUTE_NAME_FOUND = 41
+ INVALID_SIGNATURE = 42
+ WRONG_NUMBER_OF_SIGNATURES = 43
+
+ def __init__(self, message, code=0, errors=None):
+ """
+ Initializes the Exception instance.
+
+ Arguments are:
+ * (str) message. Describes the error.
+ * (int) code. The code error (defined in the error class).
+ """
+ assert isinstance(message, basestring)
+ assert isinstance(code, int)
+
+ if errors is not None:
+ message = message % errors
+
+ Exception.__init__(self, message)
+ self.code = code
\ No newline at end of file
diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py
index 99339e54..785c7c64 100644
--- a/src/onelogin/saml2/logout_request.py
+++ b/src/onelogin/saml2/logout_request.py
@@ -17,6 +17,7 @@
from onelogin.saml2.constants import OneLogin_Saml2_Constants
from onelogin.saml2.utils import OneLogin_Saml2_Utils
+from onelogin.saml2.errors import OneLogin_Saml2_Error, OneLogin_Saml2_ValidationError
class OneLogin_Saml2_Logout_Request(object):
@@ -179,7 +180,10 @@ def get_nameid_data(request, key=None):
if len(encrypted_entries) == 1:
if key is None:
- raise Exception('Key is required in order to decrypt the NameID')
+ raise OneLogin_Saml2_Error(
+ 'Private Key is required in order to decrypt the NameID, check settings',
+ OneLogin_Saml2_Error.PRIVATE_KEY_NOT_FOUND
+ )
encrypted_data_nodes = OneLogin_Saml2_Utils.query(elem, '/samlp:LogoutRequest/saml:EncryptedID/xenc:EncryptedData')
if len(encrypted_data_nodes) == 1:
@@ -191,7 +195,10 @@ def get_nameid_data(request, key=None):
name_id = entries[0]
if name_id is None:
- raise Exception('Not NameID found in the Logout Request')
+ raise OneLogin_Saml2_ValidationError(
+ 'Not NameID found in the Logout Request',
+ OneLogin_Saml2_ValidationError.NO_NAMEID
+ )
name_id_data = {
'Value': name_id.text
@@ -289,7 +296,10 @@ def is_valid(self, request_data, raise_exceptions=False):
if self.__settings.is_strict():
res = OneLogin_Saml2_Utils.validate_xml(dom, 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active())
if not isinstance(res, Document):
- raise Exception('Invalid SAML Logout Request. Not match the saml-schema-protocol-2.0.xsd')
+ raise OneLogin_Saml2_ValidationError(
+ 'Invalid SAML Logout Request. Not match the saml-schema-protocol-2.0.xsd',
+ OneLogin_Saml2_ValidationError.INVALID_XML_FORMAT
+ )
security = self.__settings.get_security_data()
@@ -318,11 +328,17 @@ def is_valid(self, request_data, raise_exceptions=False):
# Check issuer
issuer = OneLogin_Saml2_Logout_Request.get_issuer(dom)
if issuer is not None and issuer != idp_entity_id:
- raise Exception('Invalid issuer in the Logout Request')
+ raise OneLogin_Saml2_ValidationError(
+ 'Invalid issuer in the Logout Request',
+ OneLogin_Saml2_ValidationError.WRONG_ISSUER
+ )
if security['wantMessagesSigned']:
if 'Signature' not in get_data:
- raise Exception('The Message of the Logout Request is not signed and the SP require it')
+ raise OneLogin_Saml2_ValidationError(
+ 'The Message of the Logout Request is not signed and the SP require it',
+ OneLogin_Saml2_ValidationError.NO_SIGNED_RESPONSE
+ )
if 'Signature' in get_data:
if 'SigAlg' not in get_data:
@@ -336,11 +352,17 @@ def is_valid(self, request_data, raise_exceptions=False):
signed_query = '%s&SigAlg=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SigAlg', OneLogin_Saml2_Constants.RSA_SHA1, lowercase_urlencoding=lowercase_urlencoding))
if 'x509cert' not in idp_data or not idp_data['x509cert']:
- raise Exception('In order to validate the sign on the Logout Request, the x509cert of the IdP is required')
+ raise OneLogin_Saml2_Error(
+ 'In order to validate the sign on the Logout Request, the x509cert of the IdP is required',
+ OneLogin_Saml2_Error.CERT_NOT_FOUND
+ )
cert = idp_data['x509cert']
if not OneLogin_Saml2_Utils.validate_binary_sign(signed_query, b64decode(get_data['Signature']), cert, sign_alg):
- raise Exception('Signature validation failed. Logout Request rejected')
+ raise OneLogin_Saml2_ValidationError(
+ 'Signature validation failed. Logout Request rejected',
+ OneLogin_Saml2_ValidationError.INVALID_SIGNATURE
+ )
return True
except Exception as err:
diff --git a/src/onelogin/saml2/logout_response.py b/src/onelogin/saml2/logout_response.py
index 498272e0..f7fd486a 100644
--- a/src/onelogin/saml2/logout_response.py
+++ b/src/onelogin/saml2/logout_response.py
@@ -17,6 +17,7 @@
from onelogin.saml2.constants import OneLogin_Saml2_Constants
from onelogin.saml2.utils import OneLogin_Saml2_Utils
+from onelogin.saml2.errors import OneLogin_Saml2_Error, OneLogin_Saml2_ValidationError
class OneLogin_Saml2_Logout_Response(object):
@@ -91,7 +92,10 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False):
if self.__settings.is_strict():
res = OneLogin_Saml2_Utils.validate_xml(self.document, 'saml-schema-protocol-2.0.xsd', self.__settings.is_debug_active())
if not isinstance(res, Document):
- raise Exception('Invalid SAML Logout Response. Not match the saml-schema-protocol-2.0.xsd')
+ raise OneLogin_Saml2_ValidationError(
+ 'Invalid SAML Logout Response. Not match the saml-schema-protocol-2.0.xsd',
+ OneLogin_Saml2_ValidationError.INVALID_XML_FORMAT
+ )
security = self.__settings.get_security_data()
@@ -99,12 +103,18 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False):
if request_id is not None and self.document.documentElement.hasAttribute('InResponseTo'):
in_response_to = self.document.documentElement.getAttribute('InResponseTo')
if request_id != in_response_to:
- raise Exception('The InResponseTo of the Logout Response: %s, does not match the ID of the Logout request sent by the SP: %s' % (in_response_to, request_id))
+ raise OneLogin_Saml2_ValidationError(
+ 'The InResponseTo of the Logout Response: %s, does not match the ID of the Logout request sent by the SP: %s' % (in_response_to, request_id),
+ OneLogin_Saml2_ValidationError.WRONG_INRESPONSETO
+ )
# Check issuer
issuer = self.get_issuer()
if issuer is not None and issuer != idp_entity_id:
- raise Exception('Invalid issuer in the Logout Request')
+ raise OneLogin_Saml2_ValidationError(
+ 'Invalid issuer in the Logout Request',
+ OneLogin_Saml2_ValidationError.WRONG_ISSUER
+ )
current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data)
@@ -113,11 +123,17 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False):
destination = self.document.documentElement.getAttribute('Destination')
if destination != '':
if current_url not in destination:
- raise Exception('The LogoutRequest was received at $currentURL instead of $destination')
+ raise OneLogin_Saml2_ValidationError(
+ 'The LogoutResponse was received at %s instead of %s' % (current_url, destination),
+ OneLogin_Saml2_ValidationError.WRONG_DESTINATION
+ )
if security['wantMessagesSigned']:
if 'Signature' not in get_data:
- raise Exception('The Message of the Logout Response is not signed and the SP require it')
+ raise OneLogin_Saml2_ValidationError(
+ 'The Message of the Logout Response is not signed and the SP require it',
+ OneLogin_Saml2_ValidationError.NO_SIGNED_RESPONSE
+ )
if 'Signature' in get_data:
if 'SigAlg' not in get_data:
@@ -131,11 +147,17 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False):
signed_query = '%s&SigAlg=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SigAlg', OneLogin_Saml2_Constants.RSA_SHA1, lowercase_urlencoding=lowercase_urlencoding))
if 'x509cert' not in idp_data or not idp_data['x509cert']:
- raise Exception('In order to validate the sign on the Logout Response, the x509cert of the IdP is required')
+ raise OneLogin_Saml2_Error(
+ 'In order to validate the sign on the Logout Response, the x509cert of the IdP is required',
+ OneLogin_Saml2_Error.CERT_NOT_FOUND
+ )
cert = idp_data['x509cert']
if not OneLogin_Saml2_Utils.validate_binary_sign(signed_query, b64decode(get_data['Signature']), cert, sign_alg):
- raise Exception('Signature validation failed. Logout Response rejected')
+ raise OneLogin_Saml2_ValidationError(
+ 'Signature validation failed. Logout Response rejected',
+ OneLogin_Saml2_ValidationError.INVALID_SIGNATURE
+ )
return True
# pylint: disable=R0801
diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py
index bd72ac74..04522b6d 100644
--- a/src/onelogin/saml2/response.py
+++ b/src/onelogin/saml2/response.py
@@ -17,6 +17,7 @@
from onelogin.saml2.constants import OneLogin_Saml2_Constants
from onelogin.saml2.utils import OneLogin_Saml2_Utils, return_false_on_exception
+from onelogin.saml2.errors import OneLogin_Saml2_Error, OneLogin_Saml2_ValidationError
class OneLogin_Saml2_Response(object):
@@ -61,6 +62,9 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False):
:param request_id: Optional argument. The ID of the AuthNRequest sent by this SP to the IdP
:type request_id: string
+ :param raise_exceptions: Whether to return false on failure or raise an exception
+ :type raise_exceptions: Boolean
+
:returns: True if the SAML Response is valid, False if not
:rtype: bool
"""
@@ -68,15 +72,24 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False):
try:
# Checks SAML version
if self.document.get('Version', None) != '2.0':
- raise Exception('Unsupported SAML version')
+ raise OneLogin_Saml2_ValidationError(
+ 'Unsupported SAML version',
+ OneLogin_Saml2_ValidationError.UNSUPPORTED_SAML_VERSION
+ )
# Checks that ID exists
if self.document.get('ID', None) is None:
- raise Exception('Missing ID attribute on SAML Response')
+ raise OneLogin_Saml2_ValidationError(
+ 'Missing ID attribute on SAML Response',
+ OneLogin_Saml2_ValidationError.MISSING_ID
+ )
# Checks that the response only has one assertion
if not self.validate_num_assertions():
- raise Exception('SAML Response must contain 1 assertion')
+ raise OneLogin_Saml2_ValidationError(
+ 'SAML Response must contain 1 assertion',
+ OneLogin_Saml2_ValidationError.WRONG_NUMBER_OF_ASSERTIONS
+ )
# Checks that the response has the SUCCESS status
self.check_status()
@@ -99,7 +112,10 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False):
self.__settings.is_debug_active()
)
if not isinstance(res, Document):
- raise Exception(no_valid_xml_msg)
+ raise OneLogin_Saml2_ValidationError(
+ no_valid_xml_msg,
+ OneLogin_Saml2_ValidationError.INVALID_XML_FORMAT
+ )
# If encrypted, check also the decrypted document
if self.encrypted:
@@ -109,7 +125,10 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False):
self.__settings.is_debug_active()
)
if not isinstance(res, Document):
- raise Exception(no_valid_xml_msg)
+ raise OneLogin_Saml2_ValidationError(
+ no_valid_xml_msg,
+ OneLogin_Saml2_ValidationError.INVALID_XML_FORMAT
+ )
security = self.__settings.get_security_data()
current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data)
@@ -118,35 +137,56 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False):
in_response_to = self.document.get('InResponseTo', None)
if in_response_to is not None and request_id is not None:
if in_response_to != request_id:
- raise Exception('The InResponseTo of the Response: %s, does not match the ID of the AuthNRequest sent by the SP: %s' % (in_response_to, request_id))
+ raise OneLogin_Saml2_ValidationError(
+ 'The InResponseTo of the Response: %s, does not match the ID of the AuthNRequest sent by the SP: %s' % (in_response_to, request_id),
+ OneLogin_Saml2_ValidationError.WRONG_INRESPONSETO
+ )
if not self.encrypted and security.get('wantAssertionsEncrypted', False):
- raise Exception('The assertion of the Response is not encrypted and the SP require it')
+ raise OneLogin_Saml2_ValidationError(
+ 'The assertion of the Response is not encrypted and the SP require it',
+ OneLogin_Saml2_ValidationError.NO_ENCRYPTED_ASSERTION
+ )
if security.get('wantNameIdEncrypted', False):
encrypted_nameid_nodes = self.__query_assertion('/saml:Subject/saml:EncryptedID/xenc:EncryptedData')
if len(encrypted_nameid_nodes) != 1:
- raise Exception('The NameID of the Response is not encrypted and the SP require it')
+ raise OneLogin_Saml2_ValidationError(
+ 'The NameID of the Response is not encrypted and the SP require it',
+ OneLogin_Saml2_ValidationError.NO_ENCRYPTED_NAMEID
+ )
# Checks that a Conditions element exists
if not self.check_one_condition():
- raise Exception('The Assertion must include a Conditions element')
+ raise OneLogin_Saml2_ValidationError(
+ 'The Assertion must include a Conditions element',
+ OneLogin_Saml2_ValidationError.MISSING_CONDITIONS
+ )
# Validates Assertion timestamps
self.validate_timestamps(raise_exceptions=True)
# Checks that an AuthnStatement element exists and is unique
if not self.check_one_authnstatement():
- raise Exception('The Assertion must include an AuthnStatement element')
+ raise OneLogin_Saml2_ValidationError(
+ 'The Assertion must include an AuthnStatement element',
+ OneLogin_Saml2_ValidationError.WRONG_NUMBER_OF_AUTHSTATEMENTS
+ )
# Checks that there is at least one AttributeStatement if required
attribute_statement_nodes = self.__query_assertion('/saml:AttributeStatement')
if security.get('wantAttributeStatement', True) and not attribute_statement_nodes:
- raise Exception('There is no AttributeStatement on the Response')
+ raise OneLogin_Saml2_ValidationError(
+ 'There is no AttributeStatement on the Response',
+ OneLogin_Saml2_ValidationError.NO_ATTRIBUTESTATEMENT
+ )
encrypted_attributes_nodes = self.__query_assertion('/saml:AttributeStatement/saml:EncryptedAttribute')
if encrypted_attributes_nodes:
- raise Exception('There is an EncryptedAttribute in the Response and this SP not support them')
+ raise OneLogin_Saml2_ValidationError(
+ 'There is an EncryptedAttribute in the Response and this SP not support them',
+ OneLogin_Saml2_ValidationError.ENCRYPTED_ATTRIBUTES
+ )
# Checks destination
destination = self.document.get('Destination', None)
@@ -156,25 +196,40 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False):
# request_data
# current_url_routed = OneLogin_Saml2_Utils.get_self_routed_url_no_query(request_data)
# if not destination.startswith(current_url_routed):
- raise Exception('The response was received at %s instead of %s' % (current_url, destination))
+ raise OneLogin_Saml2_ValidationError(
+ 'The response was received at %s instead of %s' % (current_url, destination),
+ OneLogin_Saml2_ValidationError.WRONG_DESTINATION
+ )
elif destination == '':
- raise Exception('The response has an empty Destination value')
+ raise OneLogin_Saml2_ValidationError(
+ 'The response has an empty Destination value',
+ OneLogin_Saml2_ValidationError.EMPTY_DESTINATION
+ )
# Checks audience
valid_audiences = self.get_audiences()
if valid_audiences and sp_entity_id not in valid_audiences:
- raise Exception('%s is not a valid audience for this Response' % sp_entity_id)
+ raise OneLogin_Saml2_ValidationError(
+ '%s is not a valid audience for this Response' % sp_entity_id,
+ OneLogin_Saml2_ValidationError.WRONG_AUDIENCE
+ )
# Checks the issuers
issuers = self.get_issuers()
for issuer in issuers:
if issuer is None or issuer != idp_entity_id:
- raise Exception('Invalid issuer in the Assertion/Response')
+ raise OneLogin_Saml2_ValidationError(
+ 'Invalid issuer in the Assertion/Response',
+ OneLogin_Saml2_ValidationError.WRONG_ISSUER
+ )
# Checks the session Expiration
session_expiration = self.get_session_not_on_or_after()
if session_expiration and session_expiration <= OneLogin_Saml2_Utils.now():
- raise Exception('The attributes have expired, based on the SessionNotOnOrAfter of the AttributeStatement of this Response')
+ raise OneLogin_Saml2_ValidationError(
+ 'The attributes have expired, based on the SessionNotOnOrAfter of the AttributeStatement of this Response',
+ OneLogin_Saml2_ValidationError.SESSION_EXPIRED
+ )
# Checks the SubjectConfirmation, at least one SubjectConfirmation must be valid
any_subject_confirmation = False
@@ -208,30 +263,46 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False):
break
if not any_subject_confirmation:
- raise Exception('A valid SubjectConfirmation was not found on this Response')
+ raise OneLogin_Saml2_ValidationError(
+ 'A valid SubjectConfirmation was not found on this Response',
+ OneLogin_Saml2_ValidationError.WRONG_SUBJECTCONFIRMATION
+ )
if security.get('wantAssertionsSigned', False) and not has_signed_assertion:
- raise Exception('The Assertion of the Response is not signed and the SP require it')
+ raise OneLogin_Saml2_ValidationError(
+ 'The Assertion of the Response is not signed and the SP require it',
+ OneLogin_Saml2_ValidationError.NO_SIGNED_RESPONSE
+ )
if security.get('wantMessagesSigned', False) and not has_signed_response:
- raise Exception('The Message of the Response is not signed and the SP require it')
+ raise OneLogin_Saml2_ValidationError(
+ 'The Message of the Response is not signed and the SP require it',
+ OneLogin_Saml2_ValidationError.NO_SIGNED_ASSERTION
+ )
if not signed_elements or (not has_signed_response and not has_signed_assertion):
- raise Exception('No Signature found. SAML Response rejected')
+ raise OneLogin_Saml2_ValidationError(
+ 'No Signature found. SAML Response rejected',
+ OneLogin_Saml2_ValidationError.NO_SIGNATURE_FOUND
+ )
else:
cert = idp_data.get('x509cert', None)
fingerprint = idp_data.get('certFingerprint', None)
fingerprintalg = idp_data.get('certFingerprintAlgorithm', None)
# If find a Signature on the Response, validates it checking the original response
- if has_signed_response:
- # Raise exception if response signature is invalid
- OneLogin_Saml2_Utils.validate_sign(self.document, cert, fingerprint, fingerprintalg, xpath=OneLogin_Saml2_Utils.RESPONSE_SIGNATURE_XPATH, raise_exceptions=True)
+ if has_signed_response and not OneLogin_Saml2_Utils.validate_sign(self.document, cert, fingerprint, fingerprintalg, xpath=OneLogin_Saml2_Utils.RESPONSE_SIGNATURE_XPATH, raise_exceptions=False):
+ raise OneLogin_Saml2_ValidationError(
+ 'Signature validation failed. SAML Response rejected',
+ OneLogin_Saml2_ValidationError.INVALID_SIGNATURE
+ )
document_check_assertion = self.decrypted_document if self.encrypted else self.document
- if has_signed_assertion:
- # Raise exception if assertion signature is invalid
- OneLogin_Saml2_Utils.validate_sign(document_check_assertion, cert, fingerprint, fingerprintalg, xpath=OneLogin_Saml2_Utils.ASSERTION_SIGNATURE_XPATH, raise_exceptions=True)
+ if has_signed_assertion and not OneLogin_Saml2_Utils.validate_sign(document_check_assertion, cert, fingerprint, fingerprintalg, xpath=OneLogin_Saml2_Utils.ASSERTION_SIGNATURE_XPATH, raise_exceptions=False):
+ raise OneLogin_Saml2_ValidationError(
+ 'Signature validation failed. SAML Response rejected',
+ OneLogin_Saml2_ValidationError.INVALID_SIGNATURE
+ )
return True
except Exception as err:
@@ -258,7 +329,10 @@ def check_status(self):
status_msg = status.get('msg', None)
if status_msg:
status_exception_msg += ' -> ' + status_msg
- raise Exception(status_exception_msg)
+ raise OneLogin_Saml2_ValidationError(
+ status_exception_msg,
+ OneLogin_Saml2_ValidationError.STATUS_CODE_IS_NOT_SUCCESS
+ )
def check_one_condition(self):
"""
@@ -303,13 +377,19 @@ def get_issuers(self):
if len(message_issuer_nodes) == 1:
issuers.append(message_issuer_nodes[0].text)
else:
- raise Exception('Issuer of the Response not found or multiple.')
+ raise OneLogin_Saml2_ValidationError(
+ 'Issuer of the Response not found or multiple.',
+ OneLogin_Saml2_ValidationError.ISSUER_NOT_FOUND_IN_RESPONSE
+ )
assertion_issuer_nodes = self.__query_assertion('/saml:Issuer')
if len(assertion_issuer_nodes) == 1:
issuers.append(assertion_issuer_nodes[0].text)
else:
- raise Exception('Issuer of the Assertion not found or multiple.')
+ raise OneLogin_Saml2_ValidationError(
+ 'Issuer of the Assertion not found or multiple.',
+ OneLogin_Saml2_ValidationError.ISSUER_NOT_FOUND_IN_ASSERTION
+ )
return list(set(issuers))
@@ -336,10 +416,16 @@ def get_nameid_data(self):
security = self.__settings.get_security_data()
if security.get('wantNameId', True):
- raise Exception('Not NameID found in the assertion of the Response')
+ raise OneLogin_Saml2_ValidationError(
+ 'Not NameID found in the assertion of the Response',
+ OneLogin_Saml2_ValidationError.NO_NAMEID
+ )
else:
if self.__settings.is_strict() and not nameid.text:
- raise Exception('An empty NameID value found')
+ raise OneLogin_Saml2_ValidationError(
+ 'An empty NameID value found',
+ OneLogin_Saml2_ValidationError.EMPTY_NAMEID
+ )
nameid_data = {'Value': nameid.text}
for attr in ['Format', 'SPNameQualifier', 'NameQualifier']:
@@ -349,7 +435,10 @@ def get_nameid_data(self):
sp_data = self.__settings.get_sp_data()
sp_entity_id = sp_data.get('entityId', '')
if sp_entity_id != value:
- raise Exception('The SPNameQualifier value mistmatch the SP entityID value.')
+ raise OneLogin_Saml2_ValidationError(
+ 'The SPNameQualifier value mistmatch the SP entityID value.',
+ OneLogin_Saml2_ValidationError.SP_NAME_QUALIFIER_NAME_MISMATCH
+ )
nameid_data[attr] = value
return nameid_data
@@ -407,7 +496,10 @@ def get_attributes(self):
for attribute_node in attribute_nodes:
attr_name = attribute_node.get('Name')
if attr_name in attributes.keys():
- raise Exception('Found an Attribute element with duplicated Name')
+ raise OneLogin_Saml2_ValidationError(
+ 'Found an Attribute element with duplicated Name',
+ OneLogin_Saml2_ValidationError.DUPLICATED_ATTRIBUTE_NAME_FOUND
+ )
values = []
for attr in attribute_node.iterchildren('{%s}AttributeValue' % OneLogin_Saml2_Constants.NSMAP['saml']):
@@ -469,14 +561,23 @@ def process_signed_elements(self):
for sign_node in sign_nodes:
signed_element = sign_node.getparent().tag
if signed_element != response_tag and signed_element != assertion_tag:
- raise Exception('Invalid Signature Element %s SAML Response rejected' % signed_element)
+ raise OneLogin_Saml2_ValidationError(
+ 'Invalid Signature Element %s SAML Response rejected' % signed_element,
+ OneLogin_Saml2_ValidationError.WRONG_SIGNED_ELEMENT
+ )
if not sign_node.getparent().get('ID'):
- raise Exception('Signed Element must contain an ID. SAML Response rejected')
+ raise OneLogin_Saml2_ValidationError(
+ 'Signed Element must contain an ID. SAML Response rejected',
+ OneLogin_Saml2_ValidationError.ID_NOT_FOUND_IN_SIGNED_ELEMENT
+ )
id_value = sign_node.getparent().get('ID')
if id_value in verified_ids:
- raise Exception('Duplicated ID. SAML Response rejected')
+ raise OneLogin_Saml2_ValidationError(
+ 'Duplicated ID. SAML Response rejected',
+ OneLogin_Saml2_ValidationError.DUPLICATED_ID_IN_SIGNED_ELEMENTS
+ )
verified_ids.append(id_value)
# Check that reference URI matches the parent ID and no duplicate References or IDs
@@ -487,17 +588,26 @@ def process_signed_elements(self):
sei = ref.get('URI')[1:]
if sei != id_value:
- raise Exception('Found an invalid Signed Element. SAML Response rejected')
+ raise OneLogin_Saml2_ValidationError(
+ 'Found an invalid Signed Element. SAML Response rejected',
+ OneLogin_Saml2_ValidationError.INVALID_SIGNED_ELEMENT
+ )
if sei in verified_seis:
- raise Exception('Duplicated Reference URI. SAML Response rejected')
+ raise OneLogin_Saml2_ValidationError(
+ 'Duplicated Reference URI. SAML Response rejected',
+ OneLogin_Saml2_ValidationError.DUPLICATED_REFERENCE_IN_SIGNED_ELEMENTS
+ )
verified_seis.append(sei)
signed_elements.append(signed_element)
if signed_elements:
if not self.validate_signed_elements(signed_elements, raise_exceptions=True):
- raise Exception('Found an unexpected Signature Element. SAML Response rejected')
+ raise OneLogin_Saml2_ValidationError(
+ 'Found an unexpected Signature Element. SAML Response rejected',
+ OneLogin_Saml2_ValidationError.UNEXPECTED_SIGNED_ELEMENT
+ )
return signed_elements
@return_false_on_exception
@@ -527,12 +637,18 @@ def validate_signed_elements(self, signed_elements):
if response_tag in signed_elements:
expected_signature_nodes = OneLogin_Saml2_Utils.query(self.document, OneLogin_Saml2_Utils.RESPONSE_SIGNATURE_XPATH)
if len(expected_signature_nodes) != 1:
- raise Exception('Unexpected number of Response signatures found. SAML Response rejected.')
+ raise OneLogin_Saml2_ValidationError(
+ 'Unexpected number of Response signatures found. SAML Response rejected.',
+ OneLogin_Saml2_ValidationError.WRONG_NUMBER_OF_SIGNATURES_IN_RESPONSE
+ )
if assertion_tag in signed_elements:
expected_signature_nodes = self.__query(OneLogin_Saml2_Utils.ASSERTION_SIGNATURE_XPATH)
if len(expected_signature_nodes) != 1:
- raise Exception('Unexpected number of Assertion signatures found. SAML Response rejected.')
+ raise OneLogin_Saml2_ValidationError(
+ 'Unexpected number of Assertion signatures found. SAML Response rejected.',
+ OneLogin_Saml2_ValidationError.WRONG_NUMBER_OF_SIGNATURES_IN_ASSERTION
+ )
return True
@@ -553,9 +669,15 @@ def validate_timestamps(self):
nb_attr = conditions_node.get('NotBefore')
nooa_attr = conditions_node.get('NotOnOrAfter')
if nb_attr and OneLogin_Saml2_Utils.parse_SAML_to_time(nb_attr) > OneLogin_Saml2_Utils.now() + OneLogin_Saml2_Constants.ALLOWED_CLOCK_DRIFT:
- raise Exception('Could not validate timestamp: not yet valid. Check system clock.')
+ raise OneLogin_Saml2_ValidationError(
+ 'Could not validate timestamp: not yet valid. Check system clock.',
+ OneLogin_Saml2_ValidationError.ASSERTION_TOO_EARLY
+ )
if nooa_attr and OneLogin_Saml2_Utils.parse_SAML_to_time(nooa_attr) + OneLogin_Saml2_Constants.ALLOWED_CLOCK_DRIFT <= OneLogin_Saml2_Utils.now():
- raise Exception('Could not validate timestamp: expired. Check system clock.')
+ raise OneLogin_Saml2_ValidationError(
+ 'Could not validate timestamp: expired. Check system clock.',
+ OneLogin_Saml2_ValidationError.ASSERTION_EXPIRED
+ )
return True
def __query_assertion(self, xpath_expr):
@@ -621,7 +743,10 @@ def __decrypt_assertion(self, dom):
debug = self.__settings.is_debug_active()
if not key:
- raise Exception('No private key available, check settings')
+ raise OneLogin_Saml2_Error(
+ 'No private key available to decrypt the assertion, check settings',
+ OneLogin_Saml2_Error.PRIVATE_KEY_NOT_FOUND
+ )
encrypted_assertion_nodes = OneLogin_Saml2_Utils.query(dom, '/samlp:Response/saml:EncryptedAssertion')
if encrypted_assertion_nodes:
@@ -629,15 +754,24 @@ def __decrypt_assertion(self, dom):
if encrypted_data_nodes:
keyinfo = OneLogin_Saml2_Utils.query(encrypted_assertion_nodes[0], '//saml:EncryptedAssertion/xenc:EncryptedData/ds:KeyInfo')
if not keyinfo:
- raise Exception('No KeyInfo present, invalid Assertion')
+ raise OneLogin_Saml2_ValidationError(
+ 'No KeyInfo present, invalid Assertion',
+ OneLogin_Saml2_ValidationError.KEYINFO_NOT_FOUND_IN_ENCRYPTED_DATA
+ )
keyinfo = keyinfo[0]
children = keyinfo.getchildren()
if not children:
- raise Exception('No child to KeyInfo, invalid Assertion')
+ raise OneLogin_Saml2_ValidationError(
+ 'KeyInfo has no children nodes, invalid Assertion',
+ OneLogin_Saml2_ValidationError.CHILDREN_NODE_NOT_FOIND_IN_KEYINFO
+ )
for child in children:
if 'RetrievalMethod' in child.tag:
if child.attrib['Type'] != 'http://www.w3.org/2001/04/xmlenc#EncryptedKey':
- raise Exception('Unsupported Retrieval Method found')
+ raise OneLogin_Saml2_ValidationError(
+ 'Unsupported Retrieval Method found',
+ OneLogin_Saml2_ValidationError.UNSUPPORTED_RETRIEVAL_METHOD
+ )
uri = child.attrib['URI']
if not uri.startswith('#'):
break
diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py
index 8e3b151a..b596e493 100644
--- a/src/onelogin/saml2/settings.py
+++ b/src/onelogin/saml2/settings.py
@@ -104,7 +104,10 @@ def __init__(self, settings=None, custom_base_path=None, sp_validation_only=Fals
','.join(self.__errors)
)
else:
- raise Exception('Unsupported settings object')
+ raise OneLogin_Saml2_Error(
+ 'Unsupported settings object',
+ OneLogin_Saml2_Error.UNSUPPORTED_SETTINGS_OBJECT
+ )
self.format_idp_cert()
self.format_sp_cert()
diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py
index 16363a4d..ddde9d60 100644
--- a/src/onelogin/saml2/utils.py
+++ b/src/onelogin/saml2/utils.py
@@ -33,7 +33,8 @@
from dm.xmlsec.binding.tmpl import EncData, Signature
from onelogin.saml2.constants import OneLogin_Saml2_Constants
-from onelogin.saml2.errors import OneLogin_Saml2_Error
+from onelogin.saml2.errors import OneLogin_Saml2_Error, OneLogin_Saml2_ValidationError
+
if not globals().get('xmlsec_setup', False):
xmlsec.initialize()
@@ -652,8 +653,10 @@ def generate_name_id(value, sp_nq, sp_format, cert=None, debug=False, nq=None):
xml = name_id_container.toxml()
elem = fromstring(xml)
+ error_callback_method = None
if debug:
- xmlsec.set_error_callback(print_xmlsec_errors)
+ error_callback_method = print_xmlsec_errors
+ xmlsec.set_error_callback(error_callback_method)
# Load the public cert
mngr = xmlsec.KeysMngr()
@@ -717,11 +720,17 @@ def get_status(dom):
status_entry = OneLogin_Saml2_Utils.query(dom, '/samlp:Response/samlp:Status')
if len(status_entry) != 1:
- raise Exception('Missing valid Status on response')
+ raise OneLogin_Saml2_ValidationError(
+ 'Missing Status on response',
+ OneLogin_Saml2_ValidationError.MISSING_STATUS
+ )
code_entry = OneLogin_Saml2_Utils.query(dom, '/samlp:Response/samlp:Status/samlp:StatusCode', status_entry[0])
if len(code_entry) != 1:
- raise Exception('Missing valid Status Code on response')
+ raise OneLogin_Saml2_ValidationError(
+ 'Missing Status Code on response',
+ OneLogin_Saml2_ValidationError.MISSING_STATUS_CODE
+ )
code = code_entry[0].values()[0]
status['code'] = code
@@ -758,8 +767,10 @@ def decrypt_element(encrypted_data, key, debug=False):
elif isinstance(encrypted_data, basestring):
encrypted_data = fromstring(str(encrypted_data))
+ error_callback_method = None
if debug:
- xmlsec.set_error_callback(print_xmlsec_errors)
+ error_callback_method = print_xmlsec_errors
+ xmlsec.set_error_callback(error_callback_method)
mngr = xmlsec.KeysMngr()
@@ -831,8 +842,10 @@ def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constant
else:
raise Exception('Error parsing xml string')
+ error_callback_method = None
if debug:
- xmlsec.set_error_callback(print_xmlsec_errors)
+ error_callback_method = print_xmlsec_errors
+ xmlsec.set_error_callback(error_callback_method)
# Sign the metadata with our private key.
sign_algorithm_transform_map = {
@@ -941,8 +954,10 @@ def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', valid
else:
raise Exception('Error parsing xml string')
+ error_callback_method = None
if debug:
- xmlsec.set_error_callback(print_xmlsec_errors)
+ error_callback_method = print_xmlsec_errors
+ xmlsec.set_error_callback(error_callback_method)
xmlsec.addIDs(elem, ["ID"])
@@ -959,7 +974,7 @@ def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', valid
return OneLogin_Saml2_Utils.validate_node_sign(signature_node, elem, cert, fingerprint, fingerprintalg, validatecert, debug, raise_exceptions=True)
else:
- raise Exception('Expected exactly one signature node; got {}.'.format(len(signature_nodes)))
+ raise OneLogin_Saml2_ValidationError('Expected exactly one signature node; got {}.'.format(len(signature_nodes)), OneLogin_Saml2_ValidationError.WRONG_NUMBER_OF_SIGNATURES)
@staticmethod
@return_false_on_exception
@@ -1008,8 +1023,10 @@ def validate_metadata_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha
else:
raise Exception('Error parsing xml string')
+ error_callback_method = None
if debug:
- xmlsec.set_error_callback(print_xmlsec_errors)
+ error_callback_method = print_xmlsec_errors
+ xmlsec.set_error_callback(error_callback_method)
xmlsec.addIDs(elem, ["ID"])
@@ -1059,8 +1076,10 @@ def validate_node_sign(signature_node, elem, cert=None, fingerprint=None, finger
:param raise_exceptions: Whether to return false on failure or raise an exception
:type raise_exceptions: Boolean
"""
+ error_callback_method = None
if debug:
- xmlsec.set_error_callback(print_xmlsec_errors)
+ error_callback_method = print_xmlsec_errors
+ xmlsec.set_error_callback(error_callback_method)
xmlsec.addIDs(elem, ["ID"])
@@ -1080,7 +1099,10 @@ def validate_node_sign(signature_node, elem, cert=None, fingerprint=None, finger
# reference_elem[0].set('URI', '#%s' % signature_node.getparent().get('ID'))
if cert is None or cert == '':
- raise Exception('Could not validate node signature: No certificate provided.')
+ raise OneLogin_Saml2_Error(
+ 'Could not validate node signature: No certificate provided.',
+ OneLogin_Saml2_Error.CERT_NOT_FOUND
+ )
file_cert = OneLogin_Saml2_Utils.write_temp_file(cert)
@@ -1095,10 +1117,9 @@ def validate_node_sign(signature_node, elem, cert=None, fingerprint=None, finger
file_cert.close()
dsig_ctx.setEnabledKeyData([xmlsec.KeyDataX509])
- try:
- dsig_ctx.verify(signature_node)
- except Exception:
- raise Exception('Signature validation failed. SAML Response rejected')
+
+ dsig_ctx.verify(signature_node)
+
return True
@staticmethod
@@ -1126,8 +1147,10 @@ def validate_binary_sign(signed_query, signature, cert=None, algorithm=OneLogin_
:param raise_exceptions: Whether to return false on failure or raise an exception
:type raise_exceptions: Boolean
"""
+ error_callback_method = None
if debug:
- xmlsec.set_error_callback(print_xmlsec_errors)
+ error_callback_method = print_xmlsec_errors
+ xmlsec.set_error_callback(error_callback_method)
dsig_ctx = xmlsec.DSigCtx()
diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py
index cdd996e4..d7e50949 100644
--- a/tests/src/OneLogin/saml2_tests/auth_test.py
+++ b/tests/src/OneLogin/saml2_tests/auth_test.py
@@ -16,6 +16,7 @@
from onelogin.saml2.settings import OneLogin_Saml2_Settings
from onelogin.saml2.utils import OneLogin_Saml2_Utils
from onelogin.saml2.logout_request import OneLogin_Saml2_Logout_Request
+from onelogin.saml2.errors import OneLogin_Saml2_Error
class OneLogin_Saml2_Auth_Test(unittest.TestCase):
@@ -142,7 +143,7 @@ def testProcessNoResponse(self):
"""
auth = OneLogin_Saml2_Auth(self.get_request(), old_settings=self.loadSettingsJSON())
- with self.assertRaisesRegexp(Exception, 'SAML Response not found'):
+ with self.assertRaisesRegexp(OneLogin_Saml2_Error, 'SAML Response not found'):
auth.process_response()
self.assertEqual(auth.get_errors(), ['invalid_binding'])
@@ -257,7 +258,7 @@ def testProcessNoSLO(self):
Case No Message, An exception is throw
"""
auth = OneLogin_Saml2_Auth(self.get_request(), old_settings=self.loadSettingsJSON())
- with self.assertRaisesRegexp(Exception, 'SAML LogoutRequest/LogoutResponse not found'):
+ with self.assertRaisesRegexp(OneLogin_Saml2_Error, 'SAML LogoutRequest/LogoutResponse not found'):
auth.process_slo(True)
def testProcessSLOResponseInvalid(self):
@@ -783,7 +784,7 @@ def testLogoutNoSLO(self):
del settings_info['idp']['singleLogoutService']
auth = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings_info)
- with self.assertRaisesRegexp(Exception, 'The IdP does not support Single Log Out'):
+ with self.assertRaisesRegexp(OneLogin_Saml2_Error, 'The IdP does not support Single Log Out'):
# The Header of the redirect produces an Exception
auth.logout('http://example.com/returnto')
@@ -872,7 +873,7 @@ def testBuildRequestSignature(self):
settings['sp']['privatekey'] = ''
settings['custom_base_path'] = u'invalid/path/'
auth2 = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings)
- with self.assertRaisesRegexp(Exception, "Trying to sign the SAMLRequest but can't load the SP private key"):
+ with self.assertRaisesRegexp(OneLogin_Saml2_Error, "Trying to sign the SAMLRequest but can't load the SP private key"):
auth2.build_request_signature(message, relay_state)
def testBuildResponseSignature(self):
@@ -891,7 +892,7 @@ def testBuildResponseSignature(self):
settings['sp']['privatekey'] = ''
settings['custom_base_path'] = u'invalid/path/'
auth2 = OneLogin_Saml2_Auth(self.get_request(), old_settings=settings)
- with self.assertRaisesRegexp(Exception, "Trying to sign the SAMLResponse but can't load the SP private key"):
+ with self.assertRaisesRegexp(OneLogin_Saml2_Error, "Trying to sign the SAMLResponse but can't load the SP private key"):
auth2.build_response_signature(message, relay_state)
def testGetLastSAMLResponse(self):
diff --git a/tests/src/OneLogin/saml2_tests/logout_request_test.py b/tests/src/OneLogin/saml2_tests/logout_request_test.py
index cbdfdb33..8c484b94 100644
--- a/tests/src/OneLogin/saml2_tests/logout_request_test.py
+++ b/tests/src/OneLogin/saml2_tests/logout_request_test.py
@@ -15,6 +15,7 @@
from onelogin.saml2.logout_request import OneLogin_Saml2_Logout_Request
from onelogin.saml2.settings import OneLogin_Saml2_Settings
from onelogin.saml2.utils import OneLogin_Saml2_Utils
+from onelogin.saml2.errors import OneLogin_Saml2_Error, OneLogin_Saml2_ValidationError
class OneLogin_Saml2_Logout_Request_Test(unittest.TestCase):
@@ -113,7 +114,7 @@ def testGetNameIdData(self):
self.assertEqual(expected_name_id_data, name_id_data_2)
request_2 = self.file_contents(join(self.data_path, 'logout_requests', 'logout_request_encrypted_nameid.xml'))
- with self.assertRaisesRegexp(Exception, 'Key is required in order to decrypt the NameID'):
+ with self.assertRaisesRegexp(OneLogin_Saml2_Error, 'Key is required in order to decrypt the NameID'):
OneLogin_Saml2_Logout_Request.get_nameid_data(request_2)
settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
@@ -130,11 +131,11 @@ def testGetNameIdData(self):
encrypted_id_nodes = dom_2.getElementsByTagName('saml:EncryptedID')
encrypted_data = encrypted_id_nodes[0].firstChild.nextSibling
encrypted_id_nodes[0].removeChild(encrypted_data)
- with self.assertRaisesRegexp(Exception, 'Not NameID found in the Logout Request'):
+ with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'Not NameID found in the Logout Request'):
OneLogin_Saml2_Logout_Request.get_nameid_data(dom_2.toxml(), key)
inv_request = self.file_contents(join(self.data_path, 'logout_requests', 'invalids', 'no_nameId.xml'))
- with self.assertRaisesRegexp(Exception, 'Not NameID found in the Logout Request'):
+ with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'Not NameID found in the Logout Request'):
OneLogin_Saml2_Logout_Request.get_nameid_data(inv_request)
def testGetNameId(self):
@@ -146,7 +147,7 @@ def testGetNameId(self):
self.assertEqual(name_id, 'ONELOGIN_1e442c129e1f822c8096086a1103c5ee2c7cae1c')
request_2 = self.file_contents(join(self.data_path, 'logout_requests', 'logout_request_encrypted_nameid.xml'))
- with self.assertRaisesRegexp(Exception, 'Key is required in order to decrypt the NameID'):
+ with self.assertRaisesRegexp(OneLogin_Saml2_Error, 'Key is required in order to decrypt the NameID'):
OneLogin_Saml2_Logout_Request.get_nameid(request_2)
settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
@@ -325,7 +326,7 @@ def testIsValidRaisesExceptionWhenRaisesArgumentIsTrue(self):
self.assertFalse(logout_request.is_valid(request_data))
- with self.assertRaisesRegexp(Exception, "Invalid SAML Logout Request. Not match the saml-schema-protocol-2.0.xsd"):
+ with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, "Invalid SAML Logout Request. Not match the saml-schema-protocol-2.0.xsd"):
logout_request.is_valid(request_data, raise_exceptions=True)
def testIsValidSign(self):
diff --git a/tests/src/OneLogin/saml2_tests/logout_response_test.py b/tests/src/OneLogin/saml2_tests/logout_response_test.py
index f0a1553b..3f86df19 100644
--- a/tests/src/OneLogin/saml2_tests/logout_response_test.py
+++ b/tests/src/OneLogin/saml2_tests/logout_response_test.py
@@ -15,6 +15,7 @@
from onelogin.saml2.logout_response import OneLogin_Saml2_Logout_Response
from onelogin.saml2.settings import OneLogin_Saml2_Settings
from onelogin.saml2.utils import OneLogin_Saml2_Utils
+from onelogin.saml2.errors import OneLogin_Saml2_ValidationError
class OneLogin_Saml2_Logout_Response_Test(unittest.TestCase):
@@ -211,7 +212,7 @@ def testIsInValidDestination(self):
settings.set_strict(True)
response_2 = OneLogin_Saml2_Logout_Response(settings, message)
self.assertFalse(response_2.is_valid(request_data))
- self.assertIn('The LogoutRequest was received at', response_2.get_error())
+ self.assertIn('The LogoutResponse was received at', response_2.get_error())
# Empty destination
dom = parseString(OneLogin_Saml2_Utils.decode_base64_and_inflate(message))
@@ -242,7 +243,7 @@ def testIsValidRaisesExceptionWhenRaisesArgumentIsTrue(self):
self.assertFalse(response.is_valid(request_data))
- with self.assertRaisesRegexp(Exception, "Invalid SAML Logout Response. Not match the saml-schema-protocol-2.0.xsd"):
+ with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, "Invalid SAML Logout Response. Not match the saml-schema-protocol-2.0.xsd"):
response.is_valid(request_data, raise_exceptions=True)
def testIsInValidSign(self):
@@ -356,7 +357,7 @@ def testIsValid(self):
settings.set_strict(True)
response_2 = OneLogin_Saml2_Logout_Response(settings, message)
self.assertFalse(response_2.is_valid(request_data))
- self.assertIn('The LogoutRequest was received at', response_2.get_error())
+ self.assertIn('The LogoutResponse was received at', response_2.get_error())
plain_message = OneLogin_Saml2_Utils.decode_base64_and_inflate(message)
current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data)
diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py
index 2bd2aa7c..2803cd3e 100644
--- a/tests/src/OneLogin/saml2_tests/response_test.py
+++ b/tests/src/OneLogin/saml2_tests/response_test.py
@@ -17,6 +17,7 @@
from onelogin.saml2.response import OneLogin_Saml2_Response
from onelogin.saml2.settings import OneLogin_Saml2_Settings
from onelogin.saml2.utils import OneLogin_Saml2_Utils
+from onelogin.saml2.errors import OneLogin_Saml2_ValidationError
class OneLogin_Saml2_Response_Test(unittest.TestCase):
@@ -99,14 +100,14 @@ def testReturnNameId(self):
xml_4 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_nameid.xml.base64'))
response_4 = OneLogin_Saml2_Response(settings, xml_4)
- with self.assertRaisesRegexp(Exception, 'Not NameID found in the assertion of the Response'):
+ with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'Not NameID found in the assertion of the Response'):
response_4.get_nameid()
json_settings['security']['wantNameId'] = True
settings = OneLogin_Saml2_Settings(json_settings)
response_5 = OneLogin_Saml2_Response(settings, xml_4)
- with self.assertRaisesRegexp(Exception, 'Not NameID found in the assertion of the Response'):
+ with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'Not NameID found in the assertion of the Response'):
response_5.get_nameid()
json_settings['security']['wantNameId'] = False
@@ -120,7 +121,7 @@ def testReturnNameId(self):
settings = OneLogin_Saml2_Settings(json_settings)
response_7 = OneLogin_Saml2_Response(settings, xml_4)
- with self.assertRaisesRegexp(Exception, 'Not NameID found in the assertion of the Response'):
+ with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'Not NameID found in the assertion of the Response'):
response_7.get_nameid()
json_settings['strict'] = True
@@ -128,12 +129,12 @@ def testReturnNameId(self):
xml_5 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'wrong_spnamequalifier.xml.base64'))
response_8 = OneLogin_Saml2_Response(settings, xml_5)
- with self.assertRaisesRegexp(Exception, 'The SPNameQualifier value mistmatch the SP entityID value'):
+ with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'The SPNameQualifier value mistmatch the SP entityID value'):
response_8.get_nameid()
xml_6 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'empty_nameid.xml.base64'))
response_9 = OneLogin_Saml2_Response(settings, xml_6)
- with self.assertRaisesRegexp(Exception, 'An empty NameID value found'):
+ with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'An empty NameID value found'):
response_9.get_nameid()
def testGetNameIdData(self):
@@ -174,14 +175,14 @@ def testGetNameIdData(self):
xml_4 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_nameid.xml.base64'))
response_4 = OneLogin_Saml2_Response(settings, xml_4)
- with self.assertRaisesRegexp(Exception, 'Not NameID found in the assertion of the Response'):
+ with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'Not NameID found in the assertion of the Response'):
response_4.get_nameid_data()
json_settings['security']['wantNameId'] = True
settings = OneLogin_Saml2_Settings(json_settings)
response_5 = OneLogin_Saml2_Response(settings, xml_4)
- with self.assertRaisesRegexp(Exception, 'Not NameID found in the assertion of the Response'):
+ with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'Not NameID found in the assertion of the Response'):
response_5.get_nameid_data()
json_settings['security']['wantNameId'] = False
@@ -195,7 +196,7 @@ def testGetNameIdData(self):
settings = OneLogin_Saml2_Settings(json_settings)
response_7 = OneLogin_Saml2_Response(settings, xml_4)
- with self.assertRaisesRegexp(Exception, 'Not NameID found in the assertion of the Response'):
+ with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'Not NameID found in the assertion of the Response'):
response_7.get_nameid_data()
json_settings['strict'] = True
@@ -203,13 +204,13 @@ def testGetNameIdData(self):
xml_5 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'wrong_spnamequalifier.xml.base64'))
response_8 = OneLogin_Saml2_Response(settings, xml_5)
- with self.assertRaisesRegexp(Exception, 'The SPNameQualifier value mistmatch the SP entityID value.'):
+ with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'The SPNameQualifier value mistmatch the SP entityID value.'):
response_8.get_nameid_data()
self.assertTrue(False)
xml_6 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'empty_nameid.xml.base64'))
response_9 = OneLogin_Saml2_Response(settings, xml_6)
- with self.assertRaisesRegexp(Exception, 'An empty NameID value found'):
+ with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'An empty NameID value found'):
response_9.get_nameid_data()
def testCheckStatus(self):
@@ -227,12 +228,12 @@ def testCheckStatus(self):
xml_2 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'status_code_responder.xml.base64'))
response_2 = OneLogin_Saml2_Response(settings, xml_2)
- with self.assertRaisesRegexp(Exception, 'The status code of the Response was not Success, was Responder'):
+ with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'The status code of the Response was not Success, was Responder'):
response_2.check_status()
xml_3 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'status_code_responer_and_msg.xml.base64'))
response_3 = OneLogin_Saml2_Response(settings, xml_3)
- with self.assertRaisesRegexp(Exception, 'The status code of the Response was not Success, was Responder -> something_is_wrong'):
+ with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'The status code of the Response was not Success, was Responder -> something_is_wrong'):
response_3.check_status()
def testCheckOneCondition(self):
@@ -343,12 +344,12 @@ def testGetIssuers(self):
xml_4 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_issuer_response.xml.base64'))
response_4 = OneLogin_Saml2_Response(settings, xml_4)
- with self.assertRaisesRegexp(Exception, 'Issuer of the Response not found or multiple.'):
+ with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'Issuer of the Response not found or multiple.'):
response_4.get_issuers()
xml_5 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_issuer_assertion.xml.base64'))
response_5 = OneLogin_Saml2_Response(settings, xml_5)
- with self.assertRaisesRegexp(Exception, 'Issuer of the Assertion not found or multiple.'):
+ with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'Issuer of the Assertion not found or multiple.'):
response_5.get_issuers()
def testGetSessionIndex(self):
@@ -692,7 +693,7 @@ def testIsInValidDuplicatedAttrs(self):
xml = self.file_contents(join(self.data_path, 'responses', 'invalids', 'duplicated_attributes.xml.base64'))
response = OneLogin_Saml2_Response(settings, xml)
self.assertTrue(response.is_valid(self.get_request_data()))
- with self.assertRaisesRegexp(Exception, 'Found an Attribute element with duplicated Name'):
+ with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'Found an Attribute element with duplicated Name'):
response.get_attributes()
def testIsInValidDestination(self):
@@ -1105,9 +1106,8 @@ def testIsInValidCert(self):
settings = OneLogin_Saml2_Settings(settings_info)
xml = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64'))
response = OneLogin_Saml2_Response(settings, xml)
-
self.assertFalse(response.is_valid(self.get_request_data()))
- self.assertIn('failed to load key from file', response.get_error())
+ self.assertIn('Signature validation failed. SAML Response rejected', response.get_error())
def testIsInValidCert2(self):
"""
@@ -1236,7 +1236,7 @@ def testIsValidRaisesExceptionWhenRaisesArgumentIsTrue(self):
self.assertFalse(response.is_valid(self.get_request_data()))
- with self.assertRaisesRegexp(Exception, "Unsupported SAML version"):
+ with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, "Unsupported SAML version"):
response.is_valid(self.get_request_data(), raise_exceptions=True)
def testIsValidSign(self):
diff --git a/tests/src/OneLogin/saml2_tests/utils_test.py b/tests/src/OneLogin/saml2_tests/utils_test.py
index 7498096f..63439f68 100644
--- a/tests/src/OneLogin/saml2_tests/utils_test.py
+++ b/tests/src/OneLogin/saml2_tests/utils_test.py
@@ -15,6 +15,7 @@
from onelogin.saml2.constants import OneLogin_Saml2_Constants
from onelogin.saml2.settings import OneLogin_Saml2_Settings
from onelogin.saml2.utils import OneLogin_Saml2_Utils
+from onelogin.saml2.errors import OneLogin_Saml2_Error, OneLogin_Saml2_ValidationError
class OneLogin_Saml2_Utils_Test(unittest.TestCase):
@@ -426,14 +427,14 @@ def testGetStatus(self):
xml_inv = b64decode(xml_inv)
dom_inv = etree.fromstring(xml_inv)
- with self.assertRaisesRegexp(Exception, 'Missing valid Status on response'):
+ with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'Missing Status on response'):
OneLogin_Saml2_Utils.get_status(dom_inv)
xml_inv2 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_status_code.xml.base64'))
xml_inv2 = b64decode(xml_inv2)
dom_inv2 = etree.fromstring(xml_inv2)
- with self.assertRaisesRegexp(Exception, 'Missing valid Status Code on response'):
+ with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'Missing Status Code on response'):
OneLogin_Saml2_Utils.get_status(dom_inv2)
def testParseDuration(self):
@@ -837,7 +838,7 @@ def testValidateSign(self):
self.assertTrue(OneLogin_Saml2_Utils.validate_metadata_sign(xml_metadata_signed, cert))
# expired cert, verified it
self.assertFalse(OneLogin_Saml2_Utils.validate_metadata_sign(xml_metadata_signed, cert, validatecert=True))
- with self.assertRaises(Exception):
+ with self.assertRaisesRegexp(Exception, "('verifying failed with return value', -1)"):
OneLogin_Saml2_Utils.validate_metadata_sign(xml_metadata_signed, cert, validatecert=True, raise_exceptions=True)
xml_metadata_signed_2 = self.file_contents(join(self.data_path, 'metadata', 'signed_metadata_settings2.xml'))
@@ -850,7 +851,7 @@ def testValidateSign(self):
self.assertTrue(OneLogin_Saml2_Utils.validate_sign(xml_response_msg_signed, cert))
# expired cert, verified it
self.assertFalse(OneLogin_Saml2_Utils.validate_sign(xml_response_msg_signed, cert, validatecert=True))
- with self.assertRaises(Exception):
+ with self.assertRaisesRegexp(Exception, "('verifying failed with return value', -1)"):
OneLogin_Saml2_Utils.validate_sign(xml_response_msg_signed, cert, validatecert=True, raise_exceptions=True)
# modified cert
@@ -860,9 +861,9 @@ def testValidateSign(self):
f.close()
self.assertFalse(OneLogin_Saml2_Utils.validate_sign(xml_response_msg_signed, cert_x))
self.assertFalse(OneLogin_Saml2_Utils.validate_sign(xml_response_msg_signed, cert_x, validatecert=True))
- with self.assertRaises(Exception):
+ with self.assertRaisesRegexp(Exception, "('signature verification failed', 2)"):
OneLogin_Saml2_Utils.validate_sign(xml_response_msg_signed, cert_x, raise_exceptions=True)
- with self.assertRaises(Exception):
+ with self.assertRaisesRegexp(Exception, "('verifying failed with return value', -1)"):
OneLogin_Saml2_Utils.validate_sign(xml_response_msg_signed, cert_x, validatecert=True, raise_exceptions=True)
xml_response_msg_signed_2 = b64decode(self.file_contents(join(self.data_path, 'responses', 'signed_message_response2.xml.base64')))
@@ -877,7 +878,7 @@ def testValidateSign(self):
self.assertTrue(OneLogin_Saml2_Utils.validate_sign(xml_response_assert_signed, cert))
# expired cert, verified it
self.assertFalse(OneLogin_Saml2_Utils.validate_sign(xml_response_assert_signed, cert, validatecert=True))
- with self.assertRaises(Exception):
+ with self.assertRaisesRegexp(Exception, "('verifying failed with return value', -1)"):
OneLogin_Saml2_Utils.validate_sign(xml_response_assert_signed, cert, validatecert=True, raise_exceptions=True)
xml_response_assert_signed_2 = b64decode(self.file_contents(join(self.data_path, 'responses', 'signed_assertion_response2.xml.base64')))
@@ -890,7 +891,7 @@ def testValidateSign(self):
self.assertTrue(OneLogin_Saml2_Utils.validate_sign(xml_response_double_signed, cert))
# expired cert, verified it
self.assertFalse(OneLogin_Saml2_Utils.validate_sign(xml_response_double_signed, cert, validatecert=True))
- with self.assertRaises(Exception):
+ with self.assertRaisesRegexp(Exception, "('verifying failed with return value', -1)"):
OneLogin_Saml2_Utils.validate_sign(xml_response_double_signed, cert, validatecert=True, raise_exceptions=True)
xml_response_double_signed_2 = b64decode(self.file_contents(join(self.data_path, 'responses', 'double_signed_response2.xml.base64')))
@@ -905,13 +906,13 @@ def testValidateSign(self):
dom.firstChild.getAttributeNode('ID').nodeValue = u'_34fg27g212d63k1f923845324475802ac0fc24530b'
# Reference validation failed
self.assertFalse(OneLogin_Saml2_Utils.validate_sign(dom, cert_2))
- with self.assertRaises(Exception):
+ with self.assertRaisesRegexp(Exception, "('verifying failed with return value', -1)"):
OneLogin_Saml2_Utils.validate_sign(dom, cert_2, raise_exceptions=True)
invalid_fingerprint = 'afe71c34ef740bc87434be13a2263d31271da1f9'
# Wrong fingerprint
self.assertFalse(OneLogin_Saml2_Utils.validate_metadata_sign(xml_metadata_signed_2, None, invalid_fingerprint))
- with self.assertRaises(Exception):
+ with self.assertRaisesRegexp(OneLogin_Saml2_Error, 'Could not validate node signature: No certificate provided.'):
OneLogin_Saml2_Utils.validate_metadata_sign(xml_metadata_signed_2, None, invalid_fingerprint, raise_exceptions=True)
dom_2 = parseString(xml_response_double_signed_2)
@@ -919,31 +920,31 @@ def testValidateSign(self):
dom_2.firstChild.firstChild.firstChild.nodeValue = 'https://example.com/other-idp'
# Modified message
self.assertFalse(OneLogin_Saml2_Utils.validate_sign(dom_2, cert_2))
- with self.assertRaises(Exception):
+ with self.assertRaisesRegexp(Exception, "('signature verification failed', 2)"):
OneLogin_Saml2_Utils.validate_sign(dom_2, cert_2, raise_exceptions=True)
# Try to validate directly the Assertion
dom_3 = parseString(xml_response_double_signed_2)
assert_elem_3 = dom_3.firstChild.firstChild.nextSibling.nextSibling.nextSibling
self.assertFalse(OneLogin_Saml2_Utils.validate_sign(assert_elem_3, cert_2))
- with self.assertRaises(Exception):
+ with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, "Expected exactly one signature node; got 0."):
OneLogin_Saml2_Utils.validate_sign(assert_elem_3, cert_2, raise_exceptions=True)
# Wrong scheme
no_signed = b64decode(self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_signature.xml.base64')))
self.assertFalse(OneLogin_Saml2_Utils.validate_sign(no_signed, cert))
- with self.assertRaises(Exception):
+ with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, "Expected exactly one signature node; got 0."):
OneLogin_Saml2_Utils.validate_sign(no_signed, cert, raise_exceptions=True)
no_key = b64decode(self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_key.xml.base64')))
self.assertFalse(OneLogin_Saml2_Utils.validate_sign(no_key, cert))
- with self.assertRaises(Exception):
+ with self.assertRaisesRegexp(Exception, "('verifying failed with return value', -1)"):
OneLogin_Saml2_Utils.validate_sign(no_key, cert, raise_exceptions=True)
# Signature Wrapping attack
wrapping_attack1 = b64decode(self.file_contents(join(self.data_path, 'responses', 'invalids', 'signature_wrapping_attack.xml.base64')))
self.assertFalse(OneLogin_Saml2_Utils.validate_sign(wrapping_attack1, cert))
- with self.assertRaises(Exception):
+ with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, "Expected exactly one signature node; got 0."):
OneLogin_Saml2_Utils.validate_sign(wrapping_attack1, cert, raise_exceptions=True)
From a0b20152b88687e079030354a5d7334fbbcd81a0 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Thu, 22 Dec 2016 09:54:21 +0100
Subject: [PATCH 073/255] Fix pep8
---
src/onelogin/saml2/errors.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/onelogin/saml2/errors.py b/src/onelogin/saml2/errors.py
index 5a859fc1..ceb2730e 100644
--- a/src/onelogin/saml2/errors.py
+++ b/src/onelogin/saml2/errors.py
@@ -125,4 +125,4 @@ def __init__(self, message, code=0, errors=None):
message = message % errors
Exception.__init__(self, message)
- self.code = code
\ No newline at end of file
+ self.code = code
From 731d01f3dfdbf5631af5529b4d775a93045ba96f Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Fri, 30 Dec 2016 00:58:13 +0100
Subject: [PATCH 074/255] Minor test fixes
---
src/onelogin/saml2/errors.py | 1 +
src/onelogin/saml2/logout_request.py | 8 ++++++--
tests/src/OneLogin/saml2_tests/response_test.py | 2 +-
3 files changed, 8 insertions(+), 3 deletions(-)
diff --git a/src/onelogin/saml2/errors.py b/src/onelogin/saml2/errors.py
index ceb2730e..10c43d11 100644
--- a/src/onelogin/saml2/errors.py
+++ b/src/onelogin/saml2/errors.py
@@ -109,6 +109,7 @@ class OneLogin_Saml2_ValidationError(Exception):
DUPLICATED_ATTRIBUTE_NAME_FOUND = 41
INVALID_SIGNATURE = 42
WRONG_NUMBER_OF_SIGNATURES = 43
+ RESPONSE_EXPIRED = 44
def __init__(self, message, code=0, errors=None):
"""
diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py
index 785c7c64..c49995e3 100644
--- a/src/onelogin/saml2/logout_request.py
+++ b/src/onelogin/saml2/logout_request.py
@@ -309,7 +309,10 @@ def is_valid(self, request_data, raise_exceptions=False):
if dom.get('NotOnOrAfter', None):
na = OneLogin_Saml2_Utils.parse_SAML_to_time(dom.get('NotOnOrAfter'))
if na <= OneLogin_Saml2_Utils.now():
- raise Exception('Could not validate timestamp: expired. Check system clock.')
+ raise OneLogin_Saml2_ValidationError(
+ 'Could not validate timestamp: expired. Check system clock.',
+ OneLogin_Saml2_ValidationError.RESPONSE_EXPIRED
+ )
# Check destination
if dom.get('Destination', None):
@@ -322,7 +325,8 @@ def is_valid(self, request_data, raise_exceptions=False):
{
'currentURL': current_url,
'destination': destination,
- }
+ },
+ OneLogin_Saml2_ValidationError.WRONG_DESTINATION
)
# Check issuer
diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py
index 2803cd3e..ce940984 100644
--- a/tests/src/OneLogin/saml2_tests/response_test.py
+++ b/tests/src/OneLogin/saml2_tests/response_test.py
@@ -61,7 +61,7 @@ def testConstruct(self):
self.assertIsInstance(response_enc, OneLogin_Saml2_Response)
- def test_get_xml_document(self):
+ def testGetXMLDocument(self):
"""
Tests that we can retrieve the raw text of an encrypted XML response
without going through intermediate steps
From 3b94b705cb2840a0faa22c3e4bd015d58a043eaf Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Fri, 30 Dec 2016 17:04:52 +0100
Subject: [PATCH 075/255] Some minor improvements based on suggestions
---
src/onelogin/saml2/errors.py | 2 ++
src/onelogin/saml2/logout_request.py | 2 +-
src/onelogin/saml2/response.py | 2 +-
src/onelogin/saml2/utils.py | 9 ++++++++-
.../src/OneLogin/saml2_tests/logout_request_test.py | 4 ++--
tests/src/OneLogin/saml2_tests/response_test.py | 12 ++++++------
6 files changed, 20 insertions(+), 11 deletions(-)
diff --git a/src/onelogin/saml2/errors.py b/src/onelogin/saml2/errors.py
index 10c43d11..cd5e2fdc 100644
--- a/src/onelogin/saml2/errors.py
+++ b/src/onelogin/saml2/errors.py
@@ -25,6 +25,8 @@ class OneLogin_Saml2_Error(Exception):
SETTINGS_INVALID_SYNTAX = 1
SETTINGS_INVALID = 2
METADATA_SP_INVALID = 3
+ # SP_CERTS_NOT_FOUND is deprecated, use CERT_NOT_FOUND instead
+ SP_CERTS_NOT_FOUND = 4
CERT_NOT_FOUND = 4
REDIRECT_INVALID_URL = 5
PUBLIC_CERT_FILE_NOT_FOUND = 6
diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py
index c49995e3..93eabbf3 100644
--- a/src/onelogin/saml2/logout_request.py
+++ b/src/onelogin/saml2/logout_request.py
@@ -196,7 +196,7 @@ def get_nameid_data(request, key=None):
if name_id is None:
raise OneLogin_Saml2_ValidationError(
- 'Not NameID found in the Logout Request',
+ 'NameID not found in the Logout Request',
OneLogin_Saml2_ValidationError.NO_NAMEID
)
diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py
index 04522b6d..70c4bdb5 100644
--- a/src/onelogin/saml2/response.py
+++ b/src/onelogin/saml2/response.py
@@ -417,7 +417,7 @@ def get_nameid_data(self):
if security.get('wantNameId', True):
raise OneLogin_Saml2_ValidationError(
- 'Not NameID found in the assertion of the Response',
+ 'NameID not found in the assertion of the Response',
OneLogin_Saml2_ValidationError.NO_NAMEID
)
else:
diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py
index ddde9d60..6fa42e2d 100644
--- a/src/onelogin/saml2/utils.py
+++ b/src/onelogin/saml2/utils.py
@@ -1118,7 +1118,14 @@ def validate_node_sign(signature_node, elem, cert=None, fingerprint=None, finger
dsig_ctx.setEnabledKeyData([xmlsec.KeyDataX509])
- dsig_ctx.verify(signature_node)
+ try:
+ dsig_ctx.verify(signature_node)
+ except Exception as err:
+ raise OneLogin_Saml2_ValidationError(
+ 'Signature validation failed. SAML Response rejected. %s',
+ OneLogin_Saml2_ValidationError.INVALID_SIGNATURE,
+ err.__str__()
+ )
return True
diff --git a/tests/src/OneLogin/saml2_tests/logout_request_test.py b/tests/src/OneLogin/saml2_tests/logout_request_test.py
index 8c484b94..b58404cc 100644
--- a/tests/src/OneLogin/saml2_tests/logout_request_test.py
+++ b/tests/src/OneLogin/saml2_tests/logout_request_test.py
@@ -131,11 +131,11 @@ def testGetNameIdData(self):
encrypted_id_nodes = dom_2.getElementsByTagName('saml:EncryptedID')
encrypted_data = encrypted_id_nodes[0].firstChild.nextSibling
encrypted_id_nodes[0].removeChild(encrypted_data)
- with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'Not NameID found in the Logout Request'):
+ with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'NameID not found in the Logout Request'):
OneLogin_Saml2_Logout_Request.get_nameid_data(dom_2.toxml(), key)
inv_request = self.file_contents(join(self.data_path, 'logout_requests', 'invalids', 'no_nameId.xml'))
- with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'Not NameID found in the Logout Request'):
+ with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'NameID not found in the Logout Request'):
OneLogin_Saml2_Logout_Request.get_nameid_data(inv_request)
def testGetNameId(self):
diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py
index ce940984..593bb12b 100644
--- a/tests/src/OneLogin/saml2_tests/response_test.py
+++ b/tests/src/OneLogin/saml2_tests/response_test.py
@@ -100,14 +100,14 @@ def testReturnNameId(self):
xml_4 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_nameid.xml.base64'))
response_4 = OneLogin_Saml2_Response(settings, xml_4)
- with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'Not NameID found in the assertion of the Response'):
+ with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'NameID not found in the assertion of the Response'):
response_4.get_nameid()
json_settings['security']['wantNameId'] = True
settings = OneLogin_Saml2_Settings(json_settings)
response_5 = OneLogin_Saml2_Response(settings, xml_4)
- with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'Not NameID found in the assertion of the Response'):
+ with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'NameID not found in the assertion of the Response'):
response_5.get_nameid()
json_settings['security']['wantNameId'] = False
@@ -121,7 +121,7 @@ def testReturnNameId(self):
settings = OneLogin_Saml2_Settings(json_settings)
response_7 = OneLogin_Saml2_Response(settings, xml_4)
- with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'Not NameID found in the assertion of the Response'):
+ with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'NameID not found in the assertion of the Response'):
response_7.get_nameid()
json_settings['strict'] = True
@@ -175,14 +175,14 @@ def testGetNameIdData(self):
xml_4 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_nameid.xml.base64'))
response_4 = OneLogin_Saml2_Response(settings, xml_4)
- with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'Not NameID found in the assertion of the Response'):
+ with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'NameID not found in the assertion of the Response'):
response_4.get_nameid_data()
json_settings['security']['wantNameId'] = True
settings = OneLogin_Saml2_Settings(json_settings)
response_5 = OneLogin_Saml2_Response(settings, xml_4)
- with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'Not NameID found in the assertion of the Response'):
+ with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'NameID not found in the assertion of the Response'):
response_5.get_nameid_data()
json_settings['security']['wantNameId'] = False
@@ -196,7 +196,7 @@ def testGetNameIdData(self):
settings = OneLogin_Saml2_Settings(json_settings)
response_7 = OneLogin_Saml2_Response(settings, xml_4)
- with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'Not NameID found in the assertion of the Response'):
+ with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'NameID not found in the assertion of the Response'):
response_7.get_nameid_data()
json_settings['strict'] = True
From cd8ea02ceadf3df1d91d42b2d58441611ca31e55 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Sat, 31 Dec 2016 10:00:29 +0100
Subject: [PATCH 076/255] Fix typo
---
src/onelogin/saml2/errors.py | 2 +-
src/onelogin/saml2/response.py | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/onelogin/saml2/errors.py b/src/onelogin/saml2/errors.py
index cd5e2fdc..532844fe 100644
--- a/src/onelogin/saml2/errors.py
+++ b/src/onelogin/saml2/errors.py
@@ -103,7 +103,7 @@ class OneLogin_Saml2_ValidationError(Exception):
NO_SIGNED_ASSERTION = 33
NO_SIGNATURE_FOUND = 34
KEYINFO_NOT_FOUND_IN_ENCRYPTED_DATA = 35
- CHILDREN_NODE_NOT_FOIND_IN_KEYINFO = 36
+ CHILDREN_NODE_NOT_FOUND_IN_KEYINFO = 36
UNSUPPORTED_RETRIEVAL_METHOD = 37
NO_NAMEID = 38
EMPTY_NAMEID = 39
diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py
index 70c4bdb5..e1b44f0f 100644
--- a/src/onelogin/saml2/response.py
+++ b/src/onelogin/saml2/response.py
@@ -763,7 +763,7 @@ def __decrypt_assertion(self, dom):
if not children:
raise OneLogin_Saml2_ValidationError(
'KeyInfo has no children nodes, invalid Assertion',
- OneLogin_Saml2_ValidationError.CHILDREN_NODE_NOT_FOIND_IN_KEYINFO
+ OneLogin_Saml2_ValidationError.CHILDREN_NODE_NOT_FOUND_IN_KEYINFO
)
for child in children:
if 'RetrievalMethod' in child.tag:
From c3e696e013885714ea25e6f830129fc434b7dc44 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Mon, 2 Jan 2017 17:22:38 +0100
Subject: [PATCH 077/255] Minor fix on docs
---
README.md | 7 +++++--
src/onelogin/saml2/response.py | 4 ++--
2 files changed, 7 insertions(+), 4 deletions(-)
diff --git a/README.md b/README.md
index bb1d9e5a..cecfc25b 100644
--- a/README.md
+++ b/README.md
@@ -833,7 +833,7 @@ Main class of OneLogin Python Toolkit
* ***get_settings*** Returns the settings info.
* ***set_strict*** Set the strict mode active/disable.
* ***get_last_request_xml*** Returns the most recently-constructed/processed XML SAML request (AuthNRequest, LogoutRequest)
-* ***get_last_response_xml*** Returns the most recently-constructed/processed XML SAML response (SAMLResponse, LogoutResponse). If the SAMLResponse was encrypted, by default tries to return the decrypted XML.
+* ***get_last_response_xml*** Returns the most recently-constructed/processed XML SAML response (SAMLResponse, LogoutResponse). If the SAMLResponse had an encrypted assertion, decrypts it.
####OneLogin_Saml2_Auth - authn_request.py####
@@ -842,7 +842,7 @@ SAML 2 Authentication Request class
* `__init__` This class handles an AuthNRequest. It builds an AuthNRequest object.
* ***get_request*** Returns unsigned AuthnRequest.
* ***get_id*** Returns the AuthNRequest ID.
-
+* ***get_xml*** Returns the XML that will be sent as part of the request.
####OneLogin_Saml2_Response - response.py####
@@ -861,6 +861,7 @@ SAML 2 Authentication Response class
* ***validate_num_assertions*** Verifies that the document only contains a single Assertion (encrypted or not)
* ***validate_timestamps*** Verifies that the document is valid according to Conditions Element
* ***get_error*** After execute a validation process, if fails this method returns the cause
+* ***get_xml_document*** Returns the SAML Response document (If contains an encrypted assertion, decrypts it).
####OneLogin_Saml2_LogoutRequest - logout_request.py####
@@ -875,6 +876,7 @@ SAML 2 Logout Request class
* ***get_session_indexes*** Gets the SessionIndexes from the Logout Request.
* ***is_valid*** Checks if the Logout Request recieved is valid.
* ***get_error*** After execute a validation process, if fails this method returns the cause.
+* ***get_xml*** Returns the XML that will be sent as part of the request or that was received at the SP
####OneLogin_Saml2_LogoutResponse - logout_response.py####
@@ -887,6 +889,7 @@ SAML 2 Logout Response class
* ***build*** Creates a Logout Response object.
* ***get_response*** Returns a Logout Response object.
* ***get_error*** After execute a validation process, if fails this method returns the cause.
+* ***get_xml*** Returns the XML that will be sent as part of the response or that was received at the SP
####OneLogin_Saml2_Settings - settings.py####
diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py
index e1b44f0f..69346bf1 100644
--- a/src/onelogin/saml2/response.py
+++ b/src/onelogin/saml2/response.py
@@ -793,10 +793,10 @@ def get_error(self):
def get_xml_document(self):
"""
- If necessary, decrypt the XML response document, and return it.
+ Returns the SAML Response document (If contains an encrypted assertion, decrypts it)
:return: Decrypted XML response document
- :rtype: string
+ :rtype: DOMDocument
"""
if self.encrypted:
return self.decrypted_document
From 667b132ca30df5672ac25e15d560a5a708b6074e Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Tue, 3 Jan 2017 17:10:01 +0100
Subject: [PATCH 078/255] Fix name
---
src/onelogin/saml2/errors.py | 2 +-
src/onelogin/saml2/logout_request.py | 2 +-
src/onelogin/saml2/logout_response.py | 2 +-
src/onelogin/saml2/response.py | 2 +-
4 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/onelogin/saml2/errors.py b/src/onelogin/saml2/errors.py
index 532844fe..b85016a6 100644
--- a/src/onelogin/saml2/errors.py
+++ b/src/onelogin/saml2/errors.py
@@ -99,7 +99,7 @@ class OneLogin_Saml2_ValidationError(Exception):
WRONG_ISSUER = 29
SESSION_EXPIRED = 30
WRONG_SUBJECTCONFIRMATION = 31
- NO_SIGNED_RESPONSE = 32
+ NO_SIGNED_MESSAGE = 32
NO_SIGNED_ASSERTION = 33
NO_SIGNATURE_FOUND = 34
KEYINFO_NOT_FOUND_IN_ENCRYPTED_DATA = 35
diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py
index 93eabbf3..8dea50ef 100644
--- a/src/onelogin/saml2/logout_request.py
+++ b/src/onelogin/saml2/logout_request.py
@@ -341,7 +341,7 @@ def is_valid(self, request_data, raise_exceptions=False):
if 'Signature' not in get_data:
raise OneLogin_Saml2_ValidationError(
'The Message of the Logout Request is not signed and the SP require it',
- OneLogin_Saml2_ValidationError.NO_SIGNED_RESPONSE
+ OneLogin_Saml2_ValidationError.NO_SIGNED_MESSAGE
)
if 'Signature' in get_data:
diff --git a/src/onelogin/saml2/logout_response.py b/src/onelogin/saml2/logout_response.py
index f7fd486a..2653b210 100644
--- a/src/onelogin/saml2/logout_response.py
+++ b/src/onelogin/saml2/logout_response.py
@@ -132,7 +132,7 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False):
if 'Signature' not in get_data:
raise OneLogin_Saml2_ValidationError(
'The Message of the Logout Response is not signed and the SP require it',
- OneLogin_Saml2_ValidationError.NO_SIGNED_RESPONSE
+ OneLogin_Saml2_ValidationError.NO_SIGNED_MESSAGE
)
if 'Signature' in get_data:
diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py
index 69346bf1..a9843dc6 100644
--- a/src/onelogin/saml2/response.py
+++ b/src/onelogin/saml2/response.py
@@ -271,7 +271,7 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False):
if security.get('wantAssertionsSigned', False) and not has_signed_assertion:
raise OneLogin_Saml2_ValidationError(
'The Assertion of the Response is not signed and the SP require it',
- OneLogin_Saml2_ValidationError.NO_SIGNED_RESPONSE
+ OneLogin_Saml2_ValidationError.NO_SIGNED_MESSAGE
)
if security.get('wantMessagesSigned', False) and not has_signed_response:
From 74305d53c6cd92e0ffd64289111b7e0dd98a0a03 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Tue, 3 Jan 2017 22:23:48 +0100
Subject: [PATCH 079/255] Typo
---
src/onelogin/saml2/response.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py
index a9843dc6..051e1ccf 100644
--- a/src/onelogin/saml2/response.py
+++ b/src/onelogin/saml2/response.py
@@ -271,13 +271,13 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False):
if security.get('wantAssertionsSigned', False) and not has_signed_assertion:
raise OneLogin_Saml2_ValidationError(
'The Assertion of the Response is not signed and the SP require it',
- OneLogin_Saml2_ValidationError.NO_SIGNED_MESSAGE
+ OneLogin_Saml2_ValidationError.NO_SIGNED_ASSERTION
)
if security.get('wantMessagesSigned', False) and not has_signed_response:
raise OneLogin_Saml2_ValidationError(
'The Message of the Response is not signed and the SP require it',
- OneLogin_Saml2_ValidationError.NO_SIGNED_ASSERTION
+ OneLogin_Saml2_ValidationError.NO_SIGNED_MESSAGE
)
if not signed_elements or (not has_signed_response and not has_signed_assertion):
From 7e31bb8cb92bc9b0fdfcb9384dad889e5709f310 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Tue, 10 Jan 2017 10:28:44 +0100
Subject: [PATCH 080/255] Add CVE reference to the README
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index cecfc25b..0c88f538 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@ Python3: [python3-saml](https://github.com/onelogin/python3-saml).
#### Warning ####
-Update python-saml to 2.2.0, this version includes a security patch that contains extra validations that will prevent signature wrapping attacks.
+Update python-saml to 2.2.0, this version includes a security patch that contains extra validations that will prevent signature wrapping attacks. [CVE-2016-1000252](https://github.com/distributedweaknessfiling/DWF-Database-Artifacts/blob/master/DWF/2016/1000252/CVE-2016-1000252.json)
python-saml < v2.2.0 is vulnerable and allows signature wrapping!
From a00e79403d53c7429a21aed53cbf71b8800bd6e2 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Wed, 11 Jan 2017 13:43:14 +0100
Subject: [PATCH 081/255] Release 2.2.1
---
changelog.md | 8 ++++++++
setup.py | 4 ++--
2 files changed, 10 insertions(+), 2 deletions(-)
diff --git a/changelog.md b/changelog.md
index 40b03d4f..5346c467 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,5 +1,13 @@
# python-saml changelog
+### 2.2.1 (Jan 11, 2017)
+* [#175]((https://github.com/onelogin/python-saml/pull/175) Optionally raise detailed exceptions vs. returning False.
+Implement a more specific exception class for handling some validation errors. Improve/Fix tests
+* [#171](https://github.com/onelogin/python-saml/pull/171) Add hooks to retrieve last-sent and last-received requests and responses
+* Improved inResponse validation on Responses
+* [#173](https://github.com/onelogin/python-saml/pull/173) Fix attributeConsumingService serviceName format in README
+
+
### 2.2.0 (Oct 14, 2016)
* Several security improvements:
* Conditions element required and unique.
diff --git a/setup.py b/setup.py
index faa5708f..dfbd4b8e 100644
--- a/setup.py
+++ b/setup.py
@@ -9,10 +9,10 @@
setup(
name='python-saml',
- version='2.2.0',
+ version='2.2.1',
description='Onelogin Python Toolkit. Add SAML support to your Python software using this library',
classifiers=[
- 'Development Status :: 4 - Beta',
+ 'Development Status :: 5 - Production/Stable',
'Intended Audience :: Developers',
'Intended Audience :: System Administrators',
'Operating System :: OS Independent',
From 7e4d502c4b64ac6075577197a0096de89fb1ad01 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Mon, 6 Mar 2017 13:28:13 +0100
Subject: [PATCH 082/255] Make the Issuer on the Response Optional
---
src/onelogin/saml2/errors.py | 2 +-
src/onelogin/saml2/response.py | 15 ++++++++-------
tests/src/OneLogin/saml2_tests/response_test.py | 4 ++--
3 files changed, 11 insertions(+), 10 deletions(-)
diff --git a/src/onelogin/saml2/errors.py b/src/onelogin/saml2/errors.py
index b85016a6..0675fb4a 100644
--- a/src/onelogin/saml2/errors.py
+++ b/src/onelogin/saml2/errors.py
@@ -94,7 +94,7 @@ class OneLogin_Saml2_ValidationError(Exception):
WRONG_DESTINATION = 24
EMPTY_DESTINATION = 25
WRONG_AUDIENCE = 26
- ISSUER_NOT_FOUND_IN_RESPONSE = 27
+ ISSUER_MULTIPLE_IN_RESPONSE = 27
ISSUER_NOT_FOUND_IN_ASSERTION = 28
WRONG_ISSUER = 29
SESSION_EXPIRED = 30
diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py
index 051e1ccf..49e4f734 100644
--- a/src/onelogin/saml2/response.py
+++ b/src/onelogin/saml2/response.py
@@ -374,13 +374,14 @@ def get_issuers(self):
issuers = []
message_issuer_nodes = OneLogin_Saml2_Utils.query(self.document, '/samlp:Response/saml:Issuer')
- if len(message_issuer_nodes) == 1:
- issuers.append(message_issuer_nodes[0].text)
- else:
- raise OneLogin_Saml2_ValidationError(
- 'Issuer of the Response not found or multiple.',
- OneLogin_Saml2_ValidationError.ISSUER_NOT_FOUND_IN_RESPONSE
- )
+ if len(message_issuer_nodes) > 0:
+ if len(message_issuer_nodes) == 1:
+ issuers.append(message_issuer_nodes[0].text)
+ else:
+ raise OneLogin_Saml2_ValidationError(
+ 'Issuer of the Response is multiple.',
+ OneLogin_Saml2_ValidationError.ISSUER_MULTIPLE_IN_RESPONSE
+ )
assertion_issuer_nodes = self.__query_assertion('/saml:Issuer')
if len(assertion_issuer_nodes) == 1:
diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py
index 593bb12b..c1538617 100644
--- a/tests/src/OneLogin/saml2_tests/response_test.py
+++ b/tests/src/OneLogin/saml2_tests/response_test.py
@@ -344,8 +344,8 @@ def testGetIssuers(self):
xml_4 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_issuer_response.xml.base64'))
response_4 = OneLogin_Saml2_Response(settings, xml_4)
- with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'Issuer of the Response not found or multiple.'):
- response_4.get_issuers()
+ response_4.get_issuers()
+ self.assertEqual(['https://pitbulk.no-ip.org/simplesaml/saml2/idp/metadata.php'], response_4.get_issuers())
xml_5 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_issuer_assertion.xml.base64'))
response_5 = OneLogin_Saml2_Response(settings, xml_5)
From 616f9fd2027ecc156765575ff57fa8a8aa8d9ea6 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Mon, 6 Mar 2017 13:33:58 +0100
Subject: [PATCH 083/255] Validate serial number as string to work around
libxml2 limitation
---
src/onelogin/saml2/schemas/xmldsig-core-schema.xsd | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/onelogin/saml2/schemas/xmldsig-core-schema.xsd b/src/onelogin/saml2/schemas/xmldsig-core-schema.xsd
index dd5254bb..6f5acc75 100644
--- a/src/onelogin/saml2/schemas/xmldsig-core-schema.xsd
+++ b/src/onelogin/saml2/schemas/xmldsig-core-schema.xsd
@@ -188,7 +188,7 @@
-
+
From c3bad3fd361185aa30f16e24e185831902bc06d5 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Mon, 6 Mar 2017 13:35:34 +0100
Subject: [PATCH 084/255] Fix pep8 issue
---
tests/src/OneLogin/saml2_tests/response_test.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py
index c1538617..bba964e4 100644
--- a/tests/src/OneLogin/saml2_tests/response_test.py
+++ b/tests/src/OneLogin/saml2_tests/response_test.py
@@ -345,7 +345,7 @@ def testGetIssuers(self):
xml_4 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_issuer_response.xml.base64'))
response_4 = OneLogin_Saml2_Response(settings, xml_4)
response_4.get_issuers()
- self.assertEqual(['https://pitbulk.no-ip.org/simplesaml/saml2/idp/metadata.php'], response_4.get_issuers())
+ self.assertEqual(['https://pitbulk.no-ip.org/simplesaml/saml2/idp/metadata.php'], response_4.get_issuers())
xml_5 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_issuer_assertion.xml.base64'))
response_5 = OneLogin_Saml2_Response(settings, xml_5)
From 162156770e01ee598ac54c56195c71faea47cfa9 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Mon, 6 Mar 2017 20:47:27 +0100
Subject: [PATCH 085/255] Minor typos on Readme
---
README.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index 0c88f538..b173844a 100644
--- a/README.md
+++ b/README.md
@@ -48,8 +48,8 @@ since 2002, but lately it is becoming popular due its advantages:
General description
-------------------
-OneLogin's SAML Python toolkit lets you turn you Python application into an SP
-(Service Provider) that can connect to a IdP (Identity Provider).
+OneLogin's SAML Python toolkit lets you turn your Python application into a SP
+(Service Provider) that can be connected to an IdP (Identity Provider).
Supports:
From a5423c366c7ad2089150c152a62cf9433706b419 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Sat, 11 Mar 2017 01:19:51 +0100
Subject: [PATCH 086/255] Fix #181 Add DigestMethod support. (Add
sign_algorithm and digest_algorithm parameters to sign_metadata and add_sign
---
README.md | 9 +++++-
demo-bottle/saml/advanced_settings.json | 3 +-
demo-django/saml/advanced_settings.json | 3 +-
demo-flask/saml/advanced_settings.json | 3 +-
src/onelogin/saml2/metadata.py | 7 +++--
src/onelogin/saml2/settings.py | 8 +++++-
src/onelogin/saml2/utils.py | 18 ++++++++++--
.../src/OneLogin/saml2_tests/metadata_test.py | 21 ++++++++++++++
tests/src/OneLogin/saml2_tests/utils_test.py | 28 +++++++++++++++++++
9 files changed, 91 insertions(+), 9 deletions(-)
diff --git a/README.md b/README.md
index b173844a..0cdd9de7 100644
--- a/README.md
+++ b/README.md
@@ -399,7 +399,14 @@ In addition to the required settings data (idp, sp), extra settings can be defin
// 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256'
// 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha384'
// 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha512'
- "signatureAlgorithm": "http://www.w3.org/2000/09/xmldsig#rsa-sha1"
+ "signatureAlgorithm": "http://www.w3.org/2000/09/xmldsig#rsa-sha1",
+
+ // Algorithm that the toolkit will use on digest process. Options:
+ // 'http://www.w3.org/2000/09/xmldsig#sha1'
+ // 'http://www.w3.org/2001/04/xmlenc#sha256'
+ // 'http://www.w3.org/2001/04/xmldsig-more#sha384'
+ // 'http://www.w3.org/2001/04/xmlenc#sha512'
+ 'digestAlgorithm' => 'http://www.w3.org/2000/09/xmldsig#sha1
},
// Contact information template, it is recommended to supply
diff --git a/demo-bottle/saml/advanced_settings.json b/demo-bottle/saml/advanced_settings.json
index 5b9396ff..47ec8c28 100644
--- a/demo-bottle/saml/advanced_settings.json
+++ b/demo-bottle/saml/advanced_settings.json
@@ -10,7 +10,8 @@
"wantNameId" : true,
"wantNameIdEncrypted": false,
"wantAssertionsEncrypted": false,
- "signatureAlgorithm": "http://www.w3.org/2000/09/xmldsig#rsa-sha1"
+ "signatureAlgorithm": "http://www.w3.org/2000/09/xmldsig#rsa-sha1",
+ "digestAlgorithm": "http://www.w3.org/2000/09/xmldsig#sha1"
},
"contactPerson": {
"technical": {
diff --git a/demo-django/saml/advanced_settings.json b/demo-django/saml/advanced_settings.json
index ed0ba4ab..7efb5d1b 100644
--- a/demo-django/saml/advanced_settings.json
+++ b/demo-django/saml/advanced_settings.json
@@ -10,7 +10,8 @@
"wantNameId" : true,
"wantNameIdEncrypted": false,
"wantAssertionsEncrypted": false,
- "signatureAlgorithm": "http://www.w3.org/2000/09/xmldsig#rsa-sha1"
+ "signatureAlgorithm": "http://www.w3.org/2000/09/xmldsig#rsa-sha1",
+ "digestAlgorithm": "http://www.w3.org/2000/09/xmldsig#sha1"
},
"contactPerson": {
"technical": {
diff --git a/demo-flask/saml/advanced_settings.json b/demo-flask/saml/advanced_settings.json
index ed0ba4ab..7efb5d1b 100644
--- a/demo-flask/saml/advanced_settings.json
+++ b/demo-flask/saml/advanced_settings.json
@@ -10,7 +10,8 @@
"wantNameId" : true,
"wantNameIdEncrypted": false,
"wantAssertionsEncrypted": false,
- "signatureAlgorithm": "http://www.w3.org/2000/09/xmldsig#rsa-sha1"
+ "signatureAlgorithm": "http://www.w3.org/2000/09/xmldsig#rsa-sha1",
+ "digestAlgorithm": "http://www.w3.org/2000/09/xmldsig#sha1"
},
"contactPerson": {
"technical": {
diff --git a/src/onelogin/saml2/metadata.py b/src/onelogin/saml2/metadata.py
index fba83145..d8bc0c75 100644
--- a/src/onelogin/saml2/metadata.py
+++ b/src/onelogin/saml2/metadata.py
@@ -202,7 +202,7 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N
return metadata
@staticmethod
- def sign_metadata(metadata, key, cert, sign_algorithm=OneLogin_Saml2_Constants.RSA_SHA1):
+ def sign_metadata(metadata, key, cert, sign_algorithm=OneLogin_Saml2_Constants.RSA_SHA1, digest_algorithm=OneLogin_Saml2_Constants.SHA1):
"""
Signs the metadata with the key/cert provided
@@ -218,10 +218,13 @@ def sign_metadata(metadata, key, cert, sign_algorithm=OneLogin_Saml2_Constants.R
:param sign_algorithm: Signature algorithm method
:type sign_algorithm: string
+ :param digest_algorithm: Digest algorithm method
+ :type digest_algorithm: string
+
:returns: Signed Metadata
:rtype: string
"""
- return OneLogin_Saml2_Utils.add_sign(metadata, key, cert, False, sign_algorithm)
+ return OneLogin_Saml2_Utils.add_sign(metadata, key, cert, False, sign_algorithm, digest_algorithm)
@staticmethod
def add_x509_key_descriptors(metadata, cert=None):
diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py
index b596e493..362819c5 100644
--- a/src/onelogin/saml2/settings.py
+++ b/src/onelogin/saml2/settings.py
@@ -283,6 +283,9 @@ def __add_default_values(self):
# Signature Algorithm
self.__security.setdefault('signatureAlgorithm', OneLogin_Saml2_Constants.RSA_SHA1)
+ # Digest Algorithm
+ self.__security.setdefault('digestAlgorithm', OneLogin_Saml2_Constants.SHA1)
+
# AttributeStatement required by default
self.__security.setdefault('wantAttributeStatement', True)
@@ -639,7 +642,10 @@ def get_sp_metadata(self):
cert_metadata_file
)
- metadata = OneLogin_Saml2_Metadata.sign_metadata(metadata, key_metadata, cert_metadata)
+ signature_algorithm = self.__security['signatureAlgorithm']
+ digest_algorithm = self.__security['digestAlgorithm']
+
+ metadata = OneLogin_Saml2_Metadata.sign_metadata(metadata, key_metadata, cert_metadata, signature_algorithm, digest_algorithm)
return metadata
diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py
index 6fa42e2d..00fefae3 100644
--- a/src/onelogin/saml2/utils.py
+++ b/src/onelogin/saml2/utils.py
@@ -797,7 +797,7 @@ def write_temp_file(content):
return f_temp
@staticmethod
- def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constants.RSA_SHA1):
+ def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constants.RSA_SHA1, digest_algorithm=OneLogin_Saml2_Constants.SHA1):
"""
Adds signature key and senders certificate to an element (Message or
Assertion).
@@ -816,6 +816,12 @@ def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constant
:param sign_algorithm: Signature algorithm method
:type sign_algorithm: string
+
+ :param digest_algorithm: Digest algorithm method
+ :type digest_algorithm: string
+
+ :returns: Signed XML
+ :rtype: string
"""
if xml is None or xml == '':
raise Exception('Empty string supplied as input')
@@ -866,7 +872,15 @@ def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constant
else:
elem[0].insert(0, signature)
- ref = signature.addReference(xmlsec.TransformSha1)
+ digest_algorithm_transform_map = {
+ OneLogin_Saml2_Constants.SHA1: xmlsec.TransformSha1,
+ OneLogin_Saml2_Constants.SHA256: xmlsec.TransformSha256,
+ OneLogin_Saml2_Constants.SHA384: xmlsec.TransformSha384,
+ OneLogin_Saml2_Constants.SHA512: xmlsec.TransformSha512
+ }
+ digest_algorithm_transform = digest_algorithm_transform_map.get(digest_algorithm, xmlsec.TransformSha1)
+
+ ref = signature.addReference(digest_algorithm_transform)
ref.addTransform(xmlsec.TransformEnveloped)
ref.addTransform(xmlsec.TransformExclC14N)
diff --git a/tests/src/OneLogin/saml2_tests/metadata_test.py b/tests/src/OneLogin/saml2_tests/metadata_test.py
index 85c951cd..02e551e7 100644
--- a/tests/src/OneLogin/saml2_tests/metadata_test.py
+++ b/tests/src/OneLogin/saml2_tests/metadata_test.py
@@ -14,6 +14,7 @@
from onelogin.saml2.metadata import OneLogin_Saml2_Metadata
from onelogin.saml2.settings import OneLogin_Saml2_Settings
+from onelogin.saml2.constants import OneLogin_Saml2_Constants
class OneLogin_Saml2_Metadata_Test(unittest.TestCase):
@@ -222,12 +223,32 @@ def testSignMetadata(self):
self.assertIn(' ', signed_metadata)
self.assertIn(' ', signed_metadata)
+ self.assertIn(' ', signed_metadata)
self.assertIn('\n', signed_metadata)
with self.assertRaisesRegexp(Exception, 'Empty string supplied as input'):
OneLogin_Saml2_Metadata.sign_metadata('', key, cert)
+ signed_metadata_2 = OneLogin_Saml2_Metadata.sign_metadata(metadata, key, cert, OneLogin_Saml2_Constants.RSA_SHA256, OneLogin_Saml2_Constants.SHA384)
+ self.assertIn(' ', signed_metadata_2)
+
+ self.assertIn('urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified ', signed_metadata_2)
+
+ self.assertIn(' ', signed_metadata_2)
+ self.assertIn(' ', signed_metadata_2)
+ self.assertIn(' ', signed_metadata_2)
+ self.assertIn('\n', signed_metadata_2)
+
def testAddX509KeyDescriptors(self):
"""
Tests the addX509KeyDescriptors method of the OneLogin_Saml2_Metadata
diff --git a/tests/src/OneLogin/saml2_tests/utils_test.py b/tests/src/OneLogin/saml2_tests/utils_test.py
index 63439f68..459b0996 100644
--- a/tests/src/OneLogin/saml2_tests/utils_test.py
+++ b/tests/src/OneLogin/saml2_tests/utils_test.py
@@ -810,6 +810,34 @@ def testAddSign(self):
with self.assertRaisesRegexp(Exception, 'Error parsing xml string'):
OneLogin_Saml2_Utils.add_sign(1, key, cert)
+ def testAddSignCheckAlg(self):
+ """
+ Tests the add_sign method of the OneLogin_Saml2_Utils
+ Case: Review signature & digest algorithm
+ """
+ settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
+ key = settings.get_sp_key()
+ cert = settings.get_sp_cert()
+
+ xml_authn = b64decode(self.file_contents(join(self.data_path, 'requests', 'authn_request.xml.base64')))
+ xml_authn_signed = OneLogin_Saml2_Utils.add_sign(xml_authn, key, cert)
+ self.assertIn('', xml_authn_signed)
+ self.assertIn(' ', xml_authn_signed)
+ self.assertIn(' ', xml_authn_signed)
+ self.assertIn(' ', xml_authn_signed)
+
+ xml_authn_signed_2 = OneLogin_Saml2_Utils.add_sign(xml_authn, key, cert, False, OneLogin_Saml2_Constants.RSA_SHA256, OneLogin_Saml2_Constants.SHA384)
+ self.assertIn('', xml_authn_signed_2)
+ self.assertIn(' ', xml_authn_signed_2)
+ self.assertIn(' ', xml_authn_signed_2)
+ self.assertIn(' ', xml_authn_signed_2)
+
+ xml_authn_signed_3 = OneLogin_Saml2_Utils.add_sign(xml_authn, key, cert, False, OneLogin_Saml2_Constants.RSA_SHA384, OneLogin_Saml2_Constants.SHA512)
+ self.assertIn('', xml_authn_signed_3)
+ self.assertIn(' ', xml_authn_signed_3)
+ self.assertIn(' ', xml_authn_signed_3)
+ self.assertIn(' ', xml_authn_signed_3)
+
def testValidateSign(self):
"""
Tests the validate_sign method of the OneLogin_Saml2_Utils
From d00ee51e96a2cd25870e4fa9ed7819b1bfa7a38f Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Sat, 11 Mar 2017 01:25:42 +0100
Subject: [PATCH 087/255] Update README.md
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 0cdd9de7..ee30cc5a 100644
--- a/README.md
+++ b/README.md
@@ -406,7 +406,7 @@ In addition to the required settings data (idp, sp), extra settings can be defin
// 'http://www.w3.org/2001/04/xmlenc#sha256'
// 'http://www.w3.org/2001/04/xmldsig-more#sha384'
// 'http://www.w3.org/2001/04/xmlenc#sha512'
- 'digestAlgorithm' => 'http://www.w3.org/2000/09/xmldsig#sha1
+ "digestAlgorithm": "http://www.w3.org/2000/09/xmldsig#sha1"
},
// Contact information template, it is recommended to supply
From 52e95da0c613fdef56c0ce4ddcfdea11d0ff7f4b Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Sat, 11 Mar 2017 18:48:41 +0100
Subject: [PATCH 088/255] Fix #184. Be able to provide a NameIDFormat to
LogoutRequest
---
src/onelogin/saml2/auth.py | 11 +++
src/onelogin/saml2/logout_request.py | 14 ++-
src/onelogin/saml2/response.py | 13 +++
tests/src/OneLogin/saml2_tests/auth_test.py | 92 +++++++++++++++++++
.../src/OneLogin/saml2_tests/response_test.py | 58 ++++++++++++
5 files changed, 185 insertions(+), 3 deletions(-)
diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py
index 096440eb..a0389d67 100644
--- a/src/onelogin/saml2/auth.py
+++ b/src/onelogin/saml2/auth.py
@@ -52,6 +52,7 @@ def __init__(self, request_data, old_settings=None, custom_base_path=None):
self.__settings = OneLogin_Saml2_Settings(old_settings, custom_base_path)
self.__attributes = []
self.__nameid = None
+ self.__nameid_format = None
self.__session_index = None
self.__session_expiration = None
self.__authenticated = False
@@ -97,6 +98,7 @@ def process_response(self, request_id=None):
if response.is_valid(self.__request_data, request_id):
self.__attributes = response.get_attributes()
self.__nameid = response.get_nameid()
+ self.__nameid_format = response.get_nameid_format()
self.__session_index = response.get_session_index()
self.__session_expiration = response.get_session_not_on_or_after()
self.__authenticated = True
@@ -214,6 +216,15 @@ def get_nameid(self):
"""
return self.__nameid
+ def get_nameid_format(self):
+ """
+ Returns the nameID Format.
+
+ :returns: NameID Format
+ :rtype: string|None
+ """
+ return self.__nameid_format
+
def get_session_index(self):
"""
Returns the SessionIndex from the AuthnStatement.
diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py
index 8dea50ef..a5e6181d 100644
--- a/src/onelogin/saml2/logout_request.py
+++ b/src/onelogin/saml2/logout_request.py
@@ -29,7 +29,7 @@ class OneLogin_Saml2_Logout_Request(object):
"""
- def __init__(self, settings, request=None, name_id=None, session_index=None, nq=None):
+ def __init__(self, settings, request=None, name_id=None, session_index=None, nq=None, name_id_format=None):
"""
Constructs the Logout Request object.
@@ -47,6 +47,9 @@ def __init__(self, settings, request=None, name_id=None, session_index=None, nq=
:param nq: IDP Name Qualifier
:type: string
+
+ :param name_id_format: The NameID Format that will be set in the LogoutRequest.
+ :type: string
"""
self.__settings = settings
self.__error = None
@@ -67,7 +70,10 @@ def __init__(self, settings, request=None, name_id=None, session_index=None, nq=
cert = idp_data['x509cert']
if name_id is not None:
- nameIdFormat = sp_data['NameIDFormat']
+ if name_id_format is not None:
+ nameIdFormat = name_id_format
+ else:
+ nameIdFormat = sp_data['NameIDFormat']
spNameQualifier = None
else:
name_id = idp_data['entityId']
@@ -78,7 +84,9 @@ def __init__(self, settings, request=None, name_id=None, session_index=None, nq=
name_id,
spNameQualifier,
nameIdFormat,
- cert
+ cert,
+ False,
+ nq
)
if session_index:
diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py
index 49e4f734..93f650a3 100644
--- a/src/onelogin/saml2/response.py
+++ b/src/onelogin/saml2/response.py
@@ -457,6 +457,19 @@ def get_nameid(self):
nameid_value = nameid_data['Value']
return nameid_value
+ def get_nameid_format(self):
+ """
+ Gets the NameID Format provided by the SAML Response from the IdP
+
+ :returns: NameID Format
+ :rtype: string|None
+ """
+ nameid_format = None
+ nameid_data = self.get_nameid_data()
+ if nameid_data and 'Format' in nameid_data.keys():
+ nameid_format = nameid_data['Format']
+ return nameid_format
+
def get_session_not_on_or_after(self):
"""
Gets the SessionNotOnOrAfter from the AuthnStatement
diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py
index d7e50949..135dc99c 100644
--- a/tests/src/OneLogin/saml2_tests/auth_test.py
+++ b/tests/src/OneLogin/saml2_tests/auth_test.py
@@ -857,6 +857,98 @@ def testSetStrict(self):
with self.assertRaises(AssertionError):
auth.set_strict('42')
+ def testIsAuthenticated(self):
+ """
+ Tests the is_authenticated method of the OneLogin_Saml2_Auth
+ """
+ request_data = self.get_request()
+ del request_data['get_data']
+ message = self.file_contents(join(self.data_path, 'responses', 'response1.xml.base64'))
+ request_data['post_data'] = {
+ 'SAMLResponse': message
+ }
+ auth = OneLogin_Saml2_Auth(request_data, old_settings=self.loadSettingsJSON())
+ auth.process_response()
+ self.assertFalse(auth.is_authenticated())
+
+ message = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64'))
+ request_data['post_data'] = {
+ 'SAMLResponse': message
+ }
+ auth = OneLogin_Saml2_Auth(request_data, old_settings=self.loadSettingsJSON())
+ auth.process_response()
+ self.assertTrue(auth.is_authenticated())
+
+ def testGetNameId(self):
+ """
+ Tests the get_nameid method of the OneLogin_Saml2_Auth
+ """
+ settings = self.loadSettingsJSON()
+ request_data = self.get_request()
+ del request_data['get_data']
+ message = self.file_contents(join(self.data_path, 'responses', 'response1.xml.base64'))
+ request_data['post_data'] = {
+ 'SAMLResponse': message
+ }
+ auth = OneLogin_Saml2_Auth(request_data, old_settings=settings)
+ auth.process_response()
+ self.assertFalse(auth.is_authenticated())
+ self.assertEqual(auth.get_nameid(), None)
+
+ message = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64'))
+ request_data['post_data'] = {
+ 'SAMLResponse': message
+ }
+ auth = OneLogin_Saml2_Auth(request_data, old_settings=settings)
+ auth.process_response()
+ self.assertTrue(auth.is_authenticated())
+ self.assertEqual("492882615acf31c8096b627245d76ae53036c090", auth.get_nameid())
+
+ settings_2 = self.loadSettingsJSON('settings2.json')
+ message = self.file_contents(join(self.data_path, 'responses', 'signed_message_encrypted_assertion2.xml.base64'))
+ request_data['post_data'] = {
+ 'SAMLResponse': message
+ }
+ auth = OneLogin_Saml2_Auth(request_data, old_settings=settings_2)
+ auth.process_response()
+ self.assertTrue(auth.is_authenticated())
+ self.assertEqual("25ddd7d34a7d79db69167625cda56a320adf2876", auth.get_nameid())
+
+ def testGetNameIdFormat(self):
+ """
+ Tests the get_nameid_format method of the OneLogin_Saml2_Auth
+ """
+ settings = self.loadSettingsJSON()
+ request_data = self.get_request()
+ del request_data['get_data']
+ message = self.file_contents(join(self.data_path, 'responses', 'response1.xml.base64'))
+ request_data['post_data'] = {
+ 'SAMLResponse': message
+ }
+ auth = OneLogin_Saml2_Auth(request_data, old_settings=settings)
+ auth.process_response()
+ self.assertFalse(auth.is_authenticated())
+ self.assertEqual(auth.get_nameid_format(), None)
+
+ message = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64'))
+ request_data['post_data'] = {
+ 'SAMLResponse': message
+ }
+ auth = OneLogin_Saml2_Auth(request_data, old_settings=settings)
+ auth.process_response()
+ self.assertTrue(auth.is_authenticated())
+ self.assertEqual("urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", auth.get_nameid_format())
+
+ settings_2 = self.loadSettingsJSON('settings2.json')
+ message = self.file_contents(join(self.data_path, 'responses', 'signed_message_encrypted_assertion2.xml.base64'))
+ request_data['post_data'] = {
+ 'SAMLResponse': message
+ }
+ auth = OneLogin_Saml2_Auth(request_data, old_settings=settings_2)
+ auth.process_response()
+ self.assertTrue(auth.is_authenticated())
+ self.assertEqual("urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified", auth.get_nameid_format())
+
def testBuildRequestSignature(self):
"""
Tests the build_request_signature method of the OneLogin_Saml2_Auth
diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py
index bba964e4..8c499e9e 100644
--- a/tests/src/OneLogin/saml2_tests/response_test.py
+++ b/tests/src/OneLogin/saml2_tests/response_test.py
@@ -137,6 +137,64 @@ def testReturnNameId(self):
with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'An empty NameID value found'):
response_9.get_nameid()
+ def testReturnNameIdFormat(self):
+ """
+ Tests the get_nameid_format method of the OneLogin_Saml2_Response
+ """
+ json_settings = self.loadSettingsJSON()
+
+ settings = OneLogin_Saml2_Settings(json_settings)
+ xml = self.file_contents(join(self.data_path, 'responses', 'response1.xml.base64'))
+ response = OneLogin_Saml2_Response(settings, xml)
+ self.assertEqual('urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress', response.get_nameid_format())
+
+ xml_2 = self.file_contents(join(self.data_path, 'responses', 'response_encrypted_nameid.xml.base64'))
+ response_2 = OneLogin_Saml2_Response(settings, xml_2)
+ self.assertEqual('urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified', response_2.get_nameid_format())
+
+ xml_3 = self.file_contents(join(self.data_path, 'responses', 'valid_encrypted_assertion.xml.base64'))
+ response_3 = OneLogin_Saml2_Response(settings, xml_3)
+ self.assertEqual('urn:oasis:names:tc:SAML:2.0:nameid-format:transient', response_3.get_nameid_format())
+
+ xml_4 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_nameid.xml.base64'))
+ response_4 = OneLogin_Saml2_Response(settings, xml_4)
+ with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'NameID not found in the assertion of the Response'):
+ response_4.get_nameid_format()
+
+ json_settings['security']['wantNameId'] = True
+ settings = OneLogin_Saml2_Settings(json_settings)
+
+ response_5 = OneLogin_Saml2_Response(settings, xml_4)
+ with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'NameID not found in the assertion of the Response'):
+ response_5.get_nameid_format()
+
+ json_settings['security']['wantNameId'] = False
+ settings = OneLogin_Saml2_Settings(json_settings)
+
+ response_6 = OneLogin_Saml2_Response(settings, xml_4)
+ nameid_6 = response_6.get_nameid_format()
+ self.assertIsNone(nameid_6)
+
+ del json_settings['security']['wantNameId']
+ settings = OneLogin_Saml2_Settings(json_settings)
+
+ response_7 = OneLogin_Saml2_Response(settings, xml_4)
+ with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'NameID not found in the assertion of the Response'):
+ response_7.get_nameid_format()
+
+ json_settings['strict'] = True
+ settings = OneLogin_Saml2_Settings(json_settings)
+
+ xml_5 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'wrong_spnamequalifier.xml.base64'))
+ response_8 = OneLogin_Saml2_Response(settings, xml_5)
+ with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'The SPNameQualifier value mistmatch the SP entityID value'):
+ response_8.get_nameid_format()
+
+ xml_6 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'empty_nameid.xml.base64'))
+ response_9 = OneLogin_Saml2_Response(settings, xml_6)
+ with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'An empty NameID value found'):
+ response_9.get_nameid_format()
+
def testGetNameIdData(self):
"""
Tests the get_nameid_data method of the OneLogin_Saml2_Response
From d318d3f961727bc8c3efc1e340cef9424a18d3d8 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Sat, 11 Mar 2017 20:37:27 +0100
Subject: [PATCH 089/255] Another test
---
tests/src/OneLogin/saml2_tests/logout_request_test.py | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/tests/src/OneLogin/saml2_tests/logout_request_test.py b/tests/src/OneLogin/saml2_tests/logout_request_test.py
index b58404cc..56494e19 100644
--- a/tests/src/OneLogin/saml2_tests/logout_request_test.py
+++ b/tests/src/OneLogin/saml2_tests/logout_request_test.py
@@ -138,6 +138,10 @@ def testGetNameIdData(self):
with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'NameID not found in the Logout Request'):
OneLogin_Saml2_Logout_Request.get_nameid_data(inv_request)
+ logout_request = OneLogin_Saml2_Logout_Request(settings, None, expected_name_id_data['Value'], None, expected_name_id_data['Value'], expected_name_id_data['Format'])
+ dom = parseString(logout_request.get_xml())
+ name_id_data_3 = OneLogin_Saml2_Logout_Request.get_nameid_data(dom)
+
def testGetNameId(self):
"""
Tests the get_nameid of the OneLogin_Saml2_LogoutRequest
From 2fd0105a511f7600b4f9b52afaa1b232fc1d2bab Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Sat, 11 Mar 2017 21:12:28 +0100
Subject: [PATCH 090/255] Fix test
---
tests/src/OneLogin/saml2_tests/logout_request_test.py | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/tests/src/OneLogin/saml2_tests/logout_request_test.py b/tests/src/OneLogin/saml2_tests/logout_request_test.py
index 56494e19..5b5cd56b 100644
--- a/tests/src/OneLogin/saml2_tests/logout_request_test.py
+++ b/tests/src/OneLogin/saml2_tests/logout_request_test.py
@@ -134,13 +134,21 @@ def testGetNameIdData(self):
with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'NameID not found in the Logout Request'):
OneLogin_Saml2_Logout_Request.get_nameid_data(dom_2.toxml(), key)
+ idp_data = settings.get_idp_data()
+ expected_name_id_data = {
+ 'Format': 'urn:oasis:names:tc:SAML:2.0:nameid-format:emailAddress',
+ 'NameQualifier': idp_data['entityId'],
+ 'Value': 'ONELOGIN_9c86c4542ab9d6fce07f2f7fd335287b9b3cdf69'
+ }
+
inv_request = self.file_contents(join(self.data_path, 'logout_requests', 'invalids', 'no_nameId.xml'))
with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'NameID not found in the Logout Request'):
OneLogin_Saml2_Logout_Request.get_nameid_data(inv_request)
- logout_request = OneLogin_Saml2_Logout_Request(settings, None, expected_name_id_data['Value'], None, expected_name_id_data['Value'], expected_name_id_data['Format'])
+ logout_request = OneLogin_Saml2_Logout_Request(settings, None, expected_name_id_data['Value'], None, idp_data['entityId'], expected_name_id_data['Format'])
dom = parseString(logout_request.get_xml())
name_id_data_3 = OneLogin_Saml2_Logout_Request.get_nameid_data(dom)
+ self.assertEqual(expected_name_id_data, name_id_data_3)
def testGetNameId(self):
"""
From c3019b9e2a9cacfe5a7a0d864fee9135e5f369c9 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Sun, 12 Mar 2017 11:24:09 +0100
Subject: [PATCH 091/255] More NameID Format improvements
---
src/onelogin/saml2/auth.py | 10 ++++++++--
src/onelogin/saml2/logout_request.py | 17 +++++++++++++++++
tests/src/OneLogin/saml2_tests/auth_test.py | 15 +++++++++++++++
3 files changed, 40 insertions(+), 2 deletions(-)
diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py
index a0389d67..6e56467a 100644
--- a/src/onelogin/saml2/auth.py
+++ b/src/onelogin/saml2/auth.py
@@ -318,7 +318,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):
+ def logout(self, return_to=None, name_id=None, session_index=None, nq=None, name_id_format=None):
"""
Initiates the SLO process.
@@ -334,6 +334,9 @@ def logout(self, return_to=None, name_id=None, session_index=None, nq=None):
:param nq: IDP Name Qualifier
:type: string
+ :param name_id_format: The NameID Format that will be set in the LogoutRequest.
+ :type: string
+
:returns: Redirection url
"""
slo_url = self.get_slo_url()
@@ -345,12 +348,15 @@ def logout(self, return_to=None, name_id=None, session_index=None, nq=None):
if name_id is None and self.__nameid is not None:
name_id = self.__nameid
+ if name_id_format is None and self.__nameid_format is not None:
+ name_id_format = self.__nameid_format
logout_request = OneLogin_Saml2_Logout_Request(
self.__settings,
name_id=name_id,
session_index=session_index,
- nq=nq
+ nq=nq,
+ name_id_format=name_id_format
)
self.__last_request = logout_request.get_xml()
self.__last_request_id = logout_request.id
diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py
index a5e6181d..44dd4a52 100644
--- a/src/onelogin/saml2/logout_request.py
+++ b/src/onelogin/saml2/logout_request.py
@@ -231,6 +231,23 @@ def get_nameid(request, key=None):
name_id = OneLogin_Saml2_Logout_Request.get_nameid_data(request, key)
return name_id['Value']
+ @staticmethod
+ def get_nameid_format(request, key=None):
+ """
+ Gets the NameID Format of the Logout Request Message
+ :param request: Logout Request Message
+ :type request: string|DOMDocument
+ :param key: The SP key
+ :type key: string
+ :return: Name ID Value
+ :rtype: string
+ """
+ name_id_format = None
+ name_id_data = OneLogin_Saml2_Logout_Request.get_nameid_data(request, key)
+ if name_id_data and 'Format' in name_id_data.keys():
+ name_id_format = name_id_data['Format']
+ return name_id_format
+
@staticmethod
def get_issuer(request):
"""
diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py
index 135dc99c..9c42c0bc 100644
--- a/tests/src/OneLogin/saml2_tests/auth_test.py
+++ b/tests/src/OneLogin/saml2_tests/auth_test.py
@@ -826,6 +826,7 @@ def testLogoutNameID(self):
auth.process_response()
name_id_from_response = auth.get_nameid()
+ name_id_format_from_response = auth.get_nameid_format()
target_url = auth.logout()
parsed_query = parse_qs(urlparse(target_url)[4])
@@ -833,7 +834,21 @@ def testLogoutNameID(self):
logout_request = OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query['SAMLRequest'][0])
name_id_from_request = OneLogin_Saml2_Logout_Request.get_nameid(logout_request)
+ name_id_format_from_request = OneLogin_Saml2_Logout_Request.get_nameid_format(logout_request)
self.assertEqual(name_id_from_response, name_id_from_request)
+ self.assertEqual(name_id_format_from_response, name_id_format_from_request)
+
+ new_name_id = "new_name_id"
+ new_name_id_format = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
+ target_url_2 = auth.logout(name_id=new_name_id, name_id_format=new_name_id_format)
+ parsed_query = parse_qs(urlparse(target_url_2)[4])
+ self.assertIn('SAMLRequest', parsed_query)
+ logout_request = OneLogin_Saml2_Utils.decode_base64_and_inflate(parsed_query['SAMLRequest'][0])
+
+ name_id_from_request = OneLogin_Saml2_Logout_Request.get_nameid(logout_request)
+ name_id_format_from_request = OneLogin_Saml2_Logout_Request.get_nameid_format(logout_request)
+ self.assertEqual(new_name_id, name_id_from_request)
+ self.assertEqual(new_name_id_format, name_id_format_from_request)
def testSetStrict(self):
"""
From 37edd009764874b22900228a31ce461ff853f8a7 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Sat, 8 Apr 2017 15:12:11 +0200
Subject: [PATCH 092/255] Allows underscores in URL hosts
---
src/onelogin/saml2/settings.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py
index 362819c5..1c17ec3e 100644
--- a/src/onelogin/saml2/settings.py
+++ b/src/onelogin/saml2/settings.py
@@ -25,7 +25,7 @@
# Released under a BSD 3-Clause License
url_regex = re.compile(
r'^(?:[a-z0-9\.\-]*)://' # scheme is validated separately
- r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # domain...
+ r'(?:(?:[A-Z0-9_](?:[A-Z0-9-_]{0,61}[A-Z0-9_])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # domain...
r'localhost|' # localhost...
r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|' # ...or ipv4
r'\[?[A-F0-9]*:[A-F0-9:]+\]?)' # ...or ipv6
From cebc37b67ffa440b4e0e98f5852045b7fd1b6ca3 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Sun, 16 Apr 2017 18:07:24 +0200
Subject: [PATCH 093/255] Add Pyramid demo example
---
README.md | 79 +++++++++
demo_pyramid/.coveragerc | 3 +
demo_pyramid/.gitignore | 22 +++
demo_pyramid/CHANGES.txt | 4 +
demo_pyramid/MANIFEST.in | 2 +
demo_pyramid/README.txt | 29 ++++
demo_pyramid/demo_pyramid/__init__.py | 19 +++
.../demo_pyramid/saml/advanced_settings.json | 33 ++++
demo_pyramid/demo_pyramid/saml/certs/README | 11 ++
demo_pyramid/demo_pyramid/saml/settings.json | 30 ++++
.../demo_pyramid/static/pyramid-16x16.png | Bin 0 -> 1319 bytes
demo_pyramid/demo_pyramid/static/pyramid.png | Bin 0 -> 12901 bytes
demo_pyramid/demo_pyramid/static/theme.css | 154 ++++++++++++++++++
.../demo_pyramid/templates/attrs.jinja2 | 31 ++++
.../demo_pyramid/templates/index.jinja2 | 55 +++++++
.../demo_pyramid/templates/layout.jinja2 | 64 ++++++++
demo_pyramid/demo_pyramid/views.py | 126 ++++++++++++++
demo_pyramid/development.ini | 59 +++++++
demo_pyramid/production.ini | 53 ++++++
demo_pyramid/setup.py | 45 +++++
20 files changed, 819 insertions(+)
create mode 100644 demo_pyramid/.coveragerc
create mode 100644 demo_pyramid/.gitignore
create mode 100644 demo_pyramid/CHANGES.txt
create mode 100644 demo_pyramid/MANIFEST.in
create mode 100644 demo_pyramid/README.txt
create mode 100644 demo_pyramid/demo_pyramid/__init__.py
create mode 100644 demo_pyramid/demo_pyramid/saml/advanced_settings.json
create mode 100644 demo_pyramid/demo_pyramid/saml/certs/README
create mode 100644 demo_pyramid/demo_pyramid/saml/settings.json
create mode 100644 demo_pyramid/demo_pyramid/static/pyramid-16x16.png
create mode 100644 demo_pyramid/demo_pyramid/static/pyramid.png
create mode 100644 demo_pyramid/demo_pyramid/static/theme.css
create mode 100644 demo_pyramid/demo_pyramid/templates/attrs.jinja2
create mode 100644 demo_pyramid/demo_pyramid/templates/index.jinja2
create mode 100644 demo_pyramid/demo_pyramid/templates/layout.jinja2
create mode 100644 demo_pyramid/demo_pyramid/views.py
create mode 100644 demo_pyramid/development.ini
create mode 100644 demo_pyramid/production.ini
create mode 100644 demo_pyramid/setup.py
diff --git a/README.md b/README.md
index ee30cc5a..275dd752 100644
--- a/README.md
+++ b/README.md
@@ -172,6 +172,12 @@ This folder contains a Bottle project that will be used as demo to show how to a
This folder contains a Flask project that will be used as demo to show how to add SAML support to the Flask Framework. 'index.py' is the main flask file that has all the code, this file uses the templates stored at the 'templates' folder. In the 'saml' folder we found the 'certs' folder to store the x509 public and private key, and the saml toolkit settings (settings.json and advanced_settings.json).
+
+#### demo_pyramid ####
+
+This folder contains a Pyramid project that will be used as demo to show how to add SAML support to the [Pyramid Web Framework](http://docs.pylonsproject.org/projects/pyramid/en/latest/). '\_\_init__.py' is the main file that configures the app and its routes, 'views.py' is where all the logic and SAML handling takes place, and the templates are stored in the 'templates' folder. The 'saml' folder is the same as in the other two demos.
+
+
#### setup.py ####
Setup script is the centre of all activity in building, distributing, and installing modules.
@@ -1145,3 +1151,76 @@ Once the SP is configured, the metadata of the SP is published at the /metadata
####How it works####
This demo works very similar to the flask-demo (We did it intentionally).
+
+
+### Demo Pyramid ###
+
+Unlike the other two projects, you don't need a pre-existing virtualenv to get
+up and running here, since Pyramid comes from the
+[buildout](http://www.buildout.org/en/latest/) school of thought.
+
+To run the demo you need to install Pyramid, the requirements, etc.:
+```
+ cd demo_pyramid
+ python -m venv env
+ env/bin/pip install --upgrade pip setuptools
+ env/bin/pip install -e ".[testing]"
+```
+
+Next, edit the settings in `demo_pyramid/saml/settings.json`. (Pyramid runs on
+port 6543 by default.)
+
+Now you can run the demo like this:
+```
+ env/bin/pserve development.ini
+```
+
+If that worked, the demo is now running at http://localhost:6543.
+
+####Content####
+
+The Pyramid project contains:
+
+
+* ***\_\_init__.py*** is the main Pyramid file that configures the app and its routes.
+
+* ***views.py*** is where all the SAML handling takes place.
+
+* ***templates*** is the folder where Pyramid stores the templates of the project. It was implemented a layout.jinja2 template that is extended by index.jinja2 and attrs.jinja2, the templates of our simple demo that shows messages, user attributes when available and login and logout links.
+
+* ***saml*** is a folder that contains the 'certs' folder that could be used to store the x509 public and private key, and the saml toolkit settings (settings.json and advanced_settings.json).
+
+
+####SP setup####
+
+The Onelogin's Python Toolkit allows you to provide the settings info in 2 ways: settings files or define a setting dict. In demo_pyramid the first method is used.
+
+In the views.py file we define the SAML_PATH, which will target the 'saml' folder. We require it in order to load the settings files.
+
+First we need to edit the saml/settings.json, configure the SP part and review the metadata of the IdP and complete the IdP info. Later edit the saml/advanced_settings.json files and configure the how the toolkit will work. Check the settings section of this document if you have any doubt.
+
+####IdP setup####
+
+Once the SP is configured, the metadata of the SP is published at the /metadata/ url. Based on that info, configure the IdP.
+
+####How it works####
+
+1. First time you access to the main view 'http://localhost:6543', you can select to login and return to the same view or login and be redirected to /?attrs (attrs view).
+
+ 2. When you click:
+
+ 2.1 in the first link, we access to /?sso (index view). An AuthNRequest is sent to the IdP, we authenticate at the IdP and then a Response is sent through the user's client to the SP, specifically the Assertion Consumer Service view: /?acs. Notice that a RelayState parameter is set to the url that initiated the process, the index view.
+
+ 2.2 in the second link we access to /?attrs (attrs view), we will expetience have the same process described at 2.1 with the diference that as RelayState is set the attrs url.
+
+ 3. The SAML Response is processed in the ACS /?acs, if the Response is not valid, the process stops here and a message is shown. Otherwise we are redirected to the RelayState view. a) / or b) /?attrs
+
+ 4. We are logged in the app and the user attributes are showed. At this point, we can test the single log out functionality.
+
+ The single log out funcionality could be tested by 2 ways.
+
+ 5.1 SLO Initiated by SP. Click on the "logout" link at the SP, after that a Logout Request is sent to the IdP, the session at the IdP is closed and replies through the client to the SP with a Logout Response (sent to the Single Logout Service endpoint). The SLS endpoint /?sls of the SP process the Logout Response and if is valid, close the user session of the local app. Notice that the SLO Workflow starts and ends at the SP.
+
+ 5.2 SLO Initiated by IdP. In this case, the action takes place on the IdP side, the logout process is initiated at the IdP, sends a Logout Request to the SP (SLS endpoint, /?sls). The SLS endpoint of the SP process the Logout Request and if is valid, close the session of the user at the local app and send a Logout Response to the IdP (to the SLS endpoint of the IdP). The IdP receives the Logout Response, process it and close the session at of the IdP. Notice that the SLO Workflow starts and ends at the IdP.
+
+Notice that all the SAML Requests and Responses are handled at a unique view (index) and how GET parameters are used to know the action that must be done.
diff --git a/demo_pyramid/.coveragerc b/demo_pyramid/.coveragerc
new file mode 100644
index 00000000..fd429eb0
--- /dev/null
+++ b/demo_pyramid/.coveragerc
@@ -0,0 +1,3 @@
+[run]
+source = demo_pyramid
+omit = demo_pyramid/test*
diff --git a/demo_pyramid/.gitignore b/demo_pyramid/.gitignore
new file mode 100644
index 00000000..bd31ad13
--- /dev/null
+++ b/demo_pyramid/.gitignore
@@ -0,0 +1,22 @@
+*.egg
+*.egg-info
+*.pyc
+*$py.class
+*~
+.coverage
+coverage.xml
+build/
+dist/
+.tox/
+nosetests.xml
+env*/
+tmp/
+.cache/*
+Data.fs*
+*.sublime-project
+*.sublime-workspace
+.*.sw?
+.sw?
+.DS_Store
+coverage
+test
diff --git a/demo_pyramid/CHANGES.txt b/demo_pyramid/CHANGES.txt
new file mode 100644
index 00000000..14b902fd
--- /dev/null
+++ b/demo_pyramid/CHANGES.txt
@@ -0,0 +1,4 @@
+0.0
+---
+
+- Initial version.
diff --git a/demo_pyramid/MANIFEST.in b/demo_pyramid/MANIFEST.in
new file mode 100644
index 00000000..3b3962e4
--- /dev/null
+++ b/demo_pyramid/MANIFEST.in
@@ -0,0 +1,2 @@
+include *.txt *.ini *.cfg *.rst
+recursive-include demo_pyramid *.ico *.png *.css *.gif *.jpg *.pt *.txt *.mak *.mako *.js *.html *.xml *.jinja2
diff --git a/demo_pyramid/README.txt b/demo_pyramid/README.txt
new file mode 100644
index 00000000..13d16ded
--- /dev/null
+++ b/demo_pyramid/README.txt
@@ -0,0 +1,29 @@
+demo_pyramid
+===============================
+
+Getting Started
+---------------
+
+- Change directory into your newly created project.
+
+ cd demo_pyramid
+
+- Create a Python virtual environment.
+
+ python -m venv env
+
+- Upgrade packaging tools.
+
+ env/bin/pip install --upgrade pip setuptools
+
+- Install the project in editable mode with its testing requirements.
+
+ env/bin/pip install -e ".[testing]"
+
+- Run your project's tests.
+
+ env/bin/pytest
+
+- Run your project.
+
+ env/bin/pserve development.ini
diff --git a/demo_pyramid/demo_pyramid/__init__.py b/demo_pyramid/demo_pyramid/__init__.py
new file mode 100644
index 00000000..805e51d1
--- /dev/null
+++ b/demo_pyramid/demo_pyramid/__init__.py
@@ -0,0 +1,19 @@
+from pyramid.config import Configurator
+from pyramid.session import SignedCookieSessionFactory
+
+
+session_factory = SignedCookieSessionFactory('onelogindemopytoolkit')
+
+
+def main(global_config, **settings):
+ """ This function returns a Pyramid WSGI application.
+ """
+ config = Configurator(settings=settings)
+ config.set_session_factory(session_factory)
+ config.include('pyramid_jinja2')
+ config.add_static_view('static', 'static', cache_max_age=3600)
+ config.add_route('index', '/')
+ config.add_route('attrs', '/attrs/')
+ config.add_route('metadata', '/metadata/')
+ config.scan()
+ return config.make_wsgi_app()
diff --git a/demo_pyramid/demo_pyramid/saml/advanced_settings.json b/demo_pyramid/demo_pyramid/saml/advanced_settings.json
new file mode 100644
index 00000000..3115e17e
--- /dev/null
+++ b/demo_pyramid/demo_pyramid/saml/advanced_settings.json
@@ -0,0 +1,33 @@
+{
+ "security": {
+ "nameIdEncrypted": false,
+ "authnRequestsSigned": false,
+ "logoutRequestSigned": false,
+ "logoutResponseSigned": false,
+ "signMetadata": false,
+ "wantMessagesSigned": false,
+ "wantAssertionsSigned": false,
+ "wantNameId" : true,
+ "wantNameIdEncrypted": false,
+ "wantAssertionsEncrypted": false,
+ "signatureAlgorithm": "http://www.w3.org/2000/09/xmldsig#rsa-sha1",
+ "digestAlgorithm": "http://www.w3.org/2000/09/xmldsig#sha1"
+ },
+ "contactPerson": {
+ "technical": {
+ "givenName": "technical_name",
+ "emailAddress": "technical@example.com"
+ },
+ "support": {
+ "givenName": "support_name",
+ "emailAddress": "support@example.com"
+ }
+ },
+ "organization": {
+ "en-US": {
+ "name": "sp_test",
+ "displayname": "SP test",
+ "url": "http://sp.example.com"
+ }
+ }
+}
\ No newline at end of file
diff --git a/demo_pyramid/demo_pyramid/saml/certs/README b/demo_pyramid/demo_pyramid/saml/certs/README
new file mode 100644
index 00000000..03c13737
--- /dev/null
+++ b/demo_pyramid/demo_pyramid/saml/certs/README
@@ -0,0 +1,11 @@
+Take care of this folder that could contain private key. Be sure that this folder never is published.
+
+Onelogin Python Toolkit expects that certs for the SP could be stored in this folder as:
+
+ * sp.key Private Key
+ * sp.crt Public cert
+
+Also you can use other cert to sign the metadata of the SP using the:
+
+ * metadata.key
+ * metadata.crt
diff --git a/demo_pyramid/demo_pyramid/saml/settings.json b/demo_pyramid/demo_pyramid/saml/settings.json
new file mode 100644
index 00000000..ec40b674
--- /dev/null
+++ b/demo_pyramid/demo_pyramid/saml/settings.json
@@ -0,0 +1,30 @@
+{
+ "strict": true,
+ "debug": true,
+ "sp": {
+ "entityId": "https:///metadata/",
+ "assertionConsumerService": {
+ "url": "https:///?acs",
+ "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
+ },
+ "singleLogoutService": {
+ "url": "https:///?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": ""
+ },
+ "idp": {
+ "entityId": "https://app.onelogin.com/saml/metadata/",
+ "singleSignOnService": {
+ "url": "https://app.onelogin.com/trust/saml2/http-post/sso/",
+ "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
+ },
+ "singleLogoutService": {
+ "url": "https://app.onelogin.com/trust/saml2/http-redirect/slo/",
+ "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
+ },
+ "x509cert": ""
+ }
+}
diff --git a/demo_pyramid/demo_pyramid/static/pyramid-16x16.png b/demo_pyramid/demo_pyramid/static/pyramid-16x16.png
new file mode 100644
index 0000000000000000000000000000000000000000..979203112e76ba4cfdb8cd6f108f4275e987d99a
GIT binary patch
literal 1319
zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`k|nMYCBgY=CFO}lsSJ)O`AMk?
zp1FzXsX?iUDV2pMQ*9U+n3Xd_B1$5BeXNr6bM+EIYV;~{3xK*A7;Nk-3KEmEQ%e+*
zQqwc@Y?a>c-mj#PnPRIHZt82`Ti~3Uk?B!Ylp0*+7m{3+ootz+WN)WnQ(*-(AUCxn
zQK2F?C$HG5!d3}vt`(3C64qBz04piUwpD^SD#ABF!8yMuRl!uxR5#hc&_u!9QqR!T
z(8R(}N5ROz&{*HVSl`fC*U-qyz|zXlQ~?TIxIyg#@@$ndN=gc>^!3Zj
z%k|2Q_413-^$jg8E%gnI^o@*kfhu&1EAvVcD|GXUm0>2hq!uR^WfqiV=I1GZOiWD5
zFD$Tv3bSNU;+l1ennz|zM-B0$V)JVzP|XC=H|jx7ncO3BHWAB;NpiyW)Z+ZoqGVvir744~DzI`cN=+=uFAB-e&w+(vKt_H^esM;Afr7KMf`)Hma%LWg
zuL;)R>ucqiS6q^qmz?V9Vygr+LN7Bj#mdRl*wM}0&C<--)xyxw)!5S9(bdAp)YZVy
zz`)Yd%><^`B|o_|H#M&WrZ)wl*Ab^)P+G_>0NU)5T9jFqn&MWJpQ`}&vsET;x0vHJ
z52`l>w_7Z5>eUB2MjsTjNHGl)0wy026P|8?9C*r4%>yR)B4E1CQ{KVEz`!`m)5S5Q
z;#SXOUk#T)k>lxKj4~&v95M;sSJp8lNikLV)bX~~P1`qaNLZPZ<6^QgASli8jmk<%
zZdJ~1%}JBkHhu^^q;ghbe{lMjv_0X)ug#yI+xz_S{hiM(g*saf_f6Pt#!wis>2u+!
z{;RjXUlmP?^Kq|yJMn}2uJ~!L3EU-*{+zn6Ya6$jYueOB4G#Lx+=R;oM4sqe*Sq0$
zQ2Nf{-BFQxlX@Dog;`l=SkyQh`W)n22F}BoCTQYYC=p8g*kiK(1`FEnD$cY`NZX8<>GJicU)2$Vj5iRl-7k*od
z3K)m{Gsp@4JM)eDo7z=gtG;>hu{QvcXxLU8eD@D+$BKJ@RM`zyYKvG
z-8a3ur|aAMt1Vr-yH|CEDauQLkO+_f002lzQdIf%fB4Ui0QY*V)U3(^0FZ<%L_`#&
zL`1-fj&^1i)}{b}Bq%eIA9uoG!laCfFcwoR
zb`OTl9xm%u?u}UK6Z+-0LfvF1uNzRlu;BSs+a-xXQEAzvn#Z125}lrEE$o@!cYog?
z@lko^ANF`uyQDsu%o2*s(%P^-sbKEJ1>90;Svb?qHr@sbgo4>hFv21pO(baNe1U?G_am
z$%uaYhJupCKc=2k-Lpftu1m0%A~@dHZKRf6W*s6Qm&D`7K|3
zP8#?(KABe7=AZNd-k*6CTcqHJ?f3yA6ws8mf*wHc;}7VpNW)zn=9RJ4PSI>0zxN+V
zk#)jtw`7ILRrYRCqD>sB@)+LaZvtH~TpCmeT
z5;T(}&;kNeCnT`+Is{plpj-ki?E!QC9#bi5=5IxreNAbVsKKM4p@aIXvt)VjX~
zLcj$&PM%O%3~m8hs_+6jp*DiMh>#*THuP7Kuo(0>$o&*`2|it5S+0m8|22g(K^uZ@
z;6o1l6qp_E8Ol2dBLz5X2wDO(`F*c>PlO=RH?}G2hLZu0*R!%E-GVEC+T4e?MR);V
z_^jU-j{q4)fSwlDL?FBr6^_xQgu)=RiX|@qmWrjtpcW9eMoGpx>_EeXqAIZV+wop(eQ&
zddcwQJrU|q&zm1a_C786I&8KaRWQwHi;?Yq$Niu!>Pxo{x^?XH0JL7G3nMSGE+k(f
zUy_Yz(!p+;7({Its{k~zBrv5lr7AiB!al-t5Jn%nl7ESUGkGw&`+$zo+uAQnLLE{>
z)bjDzQo)pX%9L+Y8~jzJEXj4L`Kdd};zxK*BpmUzAbJW_l-Xc?DzrF3#ROVvYz1i|
zG2!p>JkqTYcZj=4p)#n%c22V_r7crip;Odb+M8J-{$29VK@ZtjSD$_LrTfkfWNmFpri8%bWfq{-bz;
zG=eUIHw0<~$?St1Z_;ejM$&fE_SuIT%(amlVYGL(_Z#(C5>wBQegW7bxl~NRGd`Qh@8sO
z+`6hk+hoHeiq)PuHG4Tn`%qrZs+LxT_(Bd(Ki{xdzI*yTJu-iUW<)0L8m>OWDT4~*
zF$1aATP;{kn}(yBhyLY(G%H_1)}q=hRjbx3!NSzR4{{?Yj)v46H5je}8Uyq(_rMi`oz6O&CSQn6^7ABOjKl`T{3!jW>_L33Rec#ReVI^tJu7RoS3IrvY1S=CWBV}
zj(DVYB)Etlmy{64lhVbp^w-RqOvv`h52Wogrgu6?^(V`Yjk~2|lT|VLy;=@*B!r~I
z8|W`#Sbe3tvQ^jmt**N;i}CFtk8%5h^!rhlx_72eu`tO&bwSgj$pgA!#!^*MI8xg{
z1);{xPj&iN{yU`!F$wu^-<3|6j#~sZ+%?P!QyGTW(CfbAr|D$wXU}I5X&beeKU2fX
zgG|TD(mH9GwWoafEqfywNtsR+sD)f_S-1XC!ZdqS=^Mu0^-kK3?HKXM&yhzT4l@qd
zPanHneg{AGa-3PAR(@Wn(phPhch&7}+q&sGjVCclej0Ri%*4SHsn<
zivG#tyrZ`6kG}f8qNkFVv6B*?B?^c7qCd^QpIhWA;Y#4_i;5ep-F6tVd)~Ye@x&@W
zRD74;dI!Tz#&h{&=#KO}3x)5yd$@PmAOxpk0jGthtmnp|-)tuF
z1Tmvv`is|f*M_*fh^p4)UD+SflPZC8Hjg7w~i
z(0ycHzisp0{qmAY2ps|UaK_Z-`J%VVf9SpbJPluprYHE#gZtV1+4y8Tj|NGBE~`wi
z@_GJl(X6!d`Xp!3V6r~+V{~wf2=hzgeYHYA>}2UAy?BH8kwm4$WaNG1nn&&R*Nd^p
z+GwW(`UQ`^uUfv~m
z>;IhlXnZ{sdw8O7r;wN(CFtsf_;lq)ZDY2#@hj-(BO9-l&+9uSqP?V+699mW^=F3y
zq-Ed(05Fsms+!K4an_%9V_D}HiKIYqFDouet3gNdDqgq~Fhlhumg^ihwjqz23(aGJ`+0c#A)`{X@o%~NfqNYy9ju!UL
z7IwDaKm8gS*?n^6Cnx`7=s&-I`RQz7_P>^Fo&FuxYkS4<|x%%;|+Hm0`DPOm)H|7z|vxBnsje@?m?+W*VgUrGE|YPxyZ`@-LQ%osGStsgu(yO@QOyl)q#D)Ytr9GXh*}
z|0et${3k)d(c(2y!#{rg$EUwz|J2v|ZwCGj{*CY_^}LD}Zl>0nq86_S{VNJK78X9{
z|0?+>Q^d~N&QZnQ(Ae~kXMa)t2K`g}FFRWQr=7n^{>C&h=5_jHWNB*b{I~1%de#0K
z{lbPHng0g!G5=R>zSpt9D`#h7VdgGs=xi#$#=^?Z$im9V@=leFg_nhumy?^1`5!ue
z^Wcv}#L?8y+0Ieb&dyrkuP|)>G{NtfUNiMi`M;@r%zx_WZ*}#rqWuefty%%3SLXlR
z0R)f+r_;Fr0E#pzQ6W_~sMAcu7M!n%L-`n@_FrK&I5Ctcs4eqYZOO14!psI+MDrsT(i5x*g|To^~3a
zG(P=0{jmS|yL#iajCX&owCt=*MaK8c$v*)0zil)1J#>d*NO7`^HAY{0d&~02KKt_%Xg6cfO#nv8b)Ua-40z&0(uXf(uOcr&ku?L3B3P?hlUS
z>VhrlISxGTaoh|P?^iWWhV%lXOrhv(^;xDR%1buy`MO;#8IECW(wBj%OM06l;o&Dg
z_t{hIHs-|9QteQX6_vQ46z;7-9BzVR&x{29(n4cJ4FDWf=&N)~)lGI=E7`02B6go)
z=iiKw-6unW%A7Be3W)ESUXn($gAMbhoKh88cjXAs*zc36EQ}lgSmAairK0&GdX3ZA
zwh(VLk^Kt7kZ%j{M3uh4)DPeep>H;(#PQ7%c$oa4rY89lnqJwZvz`woVWhWTw+Yt4
zK4Z?lOIC7fdL{7TL>YLd1VYhMkf)>>sG7<6sCi@j;dDj?-g`#>pw+-!OoAG9N4i|~
zeV<%)x8PqKB+Fotdk_^bJG$@J!o42!876Z5@8~BdYV^iQA4M~MF4<$54r~0Ff_Q1&*4xzmrX1KOYSRpELIw>?DZ)mm
zFK_GBShr5fn}g46mJx_Pas|Y>PqDsQE4&b>`1w%aGo;@X@s;Nl*lk5$5MGxo&fZa~
z?)Y9Rmi-z7&KQGc_gS>J^@R5LdoGtFY8iZj&|=_6q0RQ1g;q89e5r$5_4S0&a)DQ`
z7*pE~UrO~c)K@vEAM(}0siTPqLN|~uSWfh>==;JS=?6%x67xnVLg0SX0;dw)(QYaD
z!fQ;uWtNbQKGxM3j!x(;)P87Q6_D+-sl;lR%V{3cV~^olt7GJBzJR;b=~9(!Rb>Wf
zO@=*jvZGFZCSDq<2iV0<23iTJvt#ydEoHlX#ZxXcgrYkL-o%LE_o$6FtCN9
zlcTA@S;BN?S9l{x?dSoWnVOEvAClv9$$|Pd-7H34>`>0(90|@(*RN^71(;U|IgZZ)
zZug2l923m((p(=P&l2{WO{6xPmUIw_2oyDR68pd+CusR0wZ5CG&93)<%Lx8~uxYBm
ze-G
zNw<06f}q@gVA8Qb(}fP=Zu`?e&__018nWFT8S_5OBn%=uSYRS5z!Bb0v0gC5z?M+*
z_hR*MbOQQ+cgez@-}LxHbl-C%xo<)%N|8DmlT8`Ch}#1YLRj4BQiMS>rL+&$SErCb
zds6l{MUU?;ZrUiKy`0766{bKXSIGo^Ur-X?36(qO3!!T2I~D=KRMED9r}@R{l2M(Nv?cylDF1VI^29xSqn9TeS38h9L$J4&L>Ec<7Uza28R
zX>DFt@0RIAo;Z~C(DO?P2CgmMFc7o>af=(<_XSJ7XHM%!_z8lwM47LPb15t<5}EP5
z(mBmYkczz4-Y%%-gr%#24WEK|C<>&1R1{AK*K5Ez1`cC0Dki|i<&CI^$DQ{58q!G1
zPkF)4^*3=x|5^040+&pK3i-8JQXY?kV@V~fF8-~4m7G1M^$kG_!tL0U*FBzY5Ku26
zH+A2H_I;>)FHp=JtWv9jOA~xBFp&B-K?yxJ_m9=}9v>~|dgrdW<2OmV=$Qe3usLq(
zLW6Q?4BnLGv923wp)FSzT=P4)zN}C*){RW?sYu>A#ATVSzWl9_XRhmuA0o;OD7
zLxy8cz=xcz4KN*P)($VF2fn0LjQ~pO@)};rCNAwaQ9Olf)P#9+`_O$hLg<%%bMP47
zSgn!5??!W#2%1kVL8EGD-Kmf-L2nP*GpusVngEF=P8VpK5!C(M5ual@B5=GPporHm
z=t{(2cJ{dK73HYOa@-jpVoMmZ@KsX#8t(7s2bzMWOw}%oFQ{3_Y;e>?kl;tZ6SSLO
zmcn?H9Wl^ru#<={zr-R7C#&^*-!wLmsgvRU#*0Ulnv1C#@Tu4Sf-_WxH&KaiVUmUZ
zR5X7Q9J8~eocTMC^+S$XGXTdBFQi;5u<
zuUs!xDLz2~RK=mVMtBYMpijukBfad#z-48x%E81X&rY9`$0U%1^mg^?
z>TX2JO+)D9AXLYF{F&M<9#$!4+xse7j^4^-9YedAJ4L^F-PJ{;L=WaM
z;CK&Bt-!AJK9kVQfq1dGvtVdg#zaF|VRwZD9#FJ8iXkihP*
zM@V+&9q|$&`z|z3lYcs6E*-+uM>Hcf>=|$57C!!~BW_baR7XD@psemUQ5y%kr>yd1
zm8R927JKP-M5?1q?ZnPx~YXH0ZFCq=^@gs+bs?4^}Op`zA_CBxkPj6Az
z;MI_gpZMYpTd<|@Mb}Fa{lJ|9ss`EgWag+hOWFkr`ZK4QWV<~AlI>!wr0lSSbV~5M?-~y9)8}?rjttq?
z?^rV9;r(VVgQc|x4RH8HiBL$Xx&jpLq#W=yr1G14m#KFleBO;R`i+iqmIV?i2Qg|H
z!YA+}qi!5KM-5*Ct%AhX7L=b!Fk;WZUfQA=B?fYSdi{{^&I)6!1IbRk93xWB*ioWC
z-?tV_L00i(^fhn8M_lA)Ko!YJ()=M8^^mw{;%=H}o12}4K5crtox2ij!9g_=0Xe3<
zID(mRnOuw1VR-}i9O*)uJe?#bf3vhkA@etj43hODQnYmrv0UzhO`^lFJ-1}P;Ac?$
zhiHPOZ>h5;`B~^>#-nzw0Fl9#U{xfJP*TACY!(t=8uUk?VW*nFi3j;vW|
zxKM;M)HFBQDik}!miY#WErQKTN!Q*z?wcS*9tmabvs|u;E>15Q2dUyGN-b@LD@g*q
zxa4N5vkh>HdiAekp$y&A`I`z15?W+r#REcsM!bqP%YJ80Y%MQ7@{cJM
z${FeHRrCiGz;e}b{EqW5)pyhjnT+AUs*_%3>c*71W<-mp=jrq=n-|I;DRkz0NMxgPIsuX!Y
zR7vp%sdh$oStk(aqcqV?4H9HKi(0<1#1S=HfO_w@=65S|gGcS03Fw+|mgy%zoO{()m0}
z>pAmc+1M0RbgWr&WvFv|yn%jBRqVrr_U;2-)vrD~xJ6k6;)b#u<|!;Kc*Tw_WsJMh
zh#POWb=0nym|w~>*-(?kSXUNZJJ@{-n~a>(h&!cg!s(_6AI94!uX?&F-##~?~`
z-~KI!D`FZ@9lPFp_&LWAt5Ug{V(<6!o2?iv1fuQ-p)HK6;`KD^3gqU5==q~=+u<_{}p9aZZg%uQAoDv{B!5p!x?
z?Yz%z9yZ?P(tX@%>`f?*m$JrC*0rn*Sn7>p}n{_>4w}@QdRjgr$IbLT@0B
z0Oc`npAp|qbd?1666a>DtY=5;QPnFpX+E7#RCSOyY}y_At!bo}rNiExdV($9kFsJ#
z5(^aR!wwzNf+!vZO%`?N+MBYG_=G{CA*6n@*p>QRKVC>-UYv`gn*zqWSE)qvor{J0Y39m3U+bmeAu*LrMJx-`c@_H+Md^&Uao
z*%a0orrd6}m9!vAH36E1zL;zOUHC1E`^ECUZDZ_0A~E17Dw>4q33h5q)O+fK&PwW|
zpPZl0($)I-E}bki!H0=GZ7=few9+nw7V;)zpeG=Rn4UsR2V@D>7D0u*BJZ#PPu`
zlA(UtVA`W21{#bu8Yk`(qWRel%T%s*TEls6i1R98B+yaoxK}IMIrP@NMsRqyf1?yM
zVnSn2At^0mnDY#-4Qpjg$drVpRBz3Rs?#6U&N_mc-!h?S-62gqi14bK}%x=*@5ABC$@^)0|}3t=BxoOn@pl_}$J#d;E@;1%Ww
zwT%8|h*_+45u3nzqKub-PLF!;LA-(HA_s1gQ+7MFcviUpD8hPx%s0Zb1__vV)25W5
zS8{t2u3;1^9m!;F=SKmH&O9g)-TOhZF0c~7o7DNtsbdX0Tn;$h`NWy72*Mr#gHQS#
zxa;YG>k!+`V6gK%<|AxR0=w-BYvnhxV_;6*wa{Yk+{0;B$x3X}7Ts=)lDo|UGy%$3
ze(nY-aNI$*KmL+r5rTu;W1F^>jJy^sKai!dL>Y>OxAuj4P4bm5R=V>w`O3fgBeEjm
zd~78>4=abld8DYhhvcp(qB}w@gIuMGrdY2^bMsi?&x!AEHF{hc
zg+DNk`;;y^5U@qHYv=l>ylIkSKv`8XcdBRg;rpD%iSyg|$79xK4KALACso;aG|J4{
zM=o}BWcpcJ_KWUFN?+hr{n&QgXhF{Bw41yMii;`_47xsc=oiVa{2v8tGY5O%13zW5
zMwu0SaukgGf@_5T!7pGgvWky^G(b8|<3Q8c?2Uxz;%QICsI2NeBU(~c(>lU$tH6(C
z!?)hYYB0_>NwsFik*o@lw6(twlS4|#5OmRiv&TD9A6z@RJWNVz&4kkC_)Ex;=+C8%
zhHW9oet8E%$zeE0Lx)l|
z=wi>IIXK<#^2zteGFqb+r$okBf*p0SN|+e-y7uB{@}g!jRmHwcAD#4Zq9gfyip+yb
zg$p2O6nybR$|(m`%9&h-k;~b0WPC*SWDv0
z1&u*-B!%bl&r%mubHAmK3X&*R)ku>#Sn8&-cW
zK2P^rNE3`Q0+iXeUh@BCMUQms&JW=t9&VE&B=j{C_Mt
z$mL|laTbhQgi+&2b%Qj`DcB8l_!W28*z<;=$}o~q$YZ5ib#+OCo=#EfVrgPSawAWd
z%WVsIlIueGb^Y3(soWx^r_q}!j>S~mK`W5qoTfHg!!)Hpx3H`rxF;vV&q!5gyXfe}
z0w`eJ`B@bPHZN8O{n}7ct|M7Y)O;n&H{BRtSwk_lvB@p=mUnMu$2+*_ZDam<*g2lV)D*CN+d5a+`ovbJ*R=J07&JhgO=jzjd`-bU(l369g@NNrgdz_k
zKoFKRlMxmvxJ37=Bl>ZIpqmbal3z$ZE^S5i&CR0o139PaX1W0jev36_8BKBDBWBgY
z+%U+5sdYYFRpUB=UK}(m(IhK;P9vq0l@!jGw&92`OV>^M^F>373n5EmP=f%-E$UZMe?68(8}P<^m%x{pAz(AgGxC!4Ig1;|%a<*AzsN%*Uyc2owx7h3Fy+Jzq=sO^hwGFuo^~
zc{n)Li1;l8scyJNbWB?Usx}BfKosHBm}Jx1lq&Bi_P^1ZB6Q=hU%}Lr@(6cSFhF3B
zQL_L=1zoL|{=*JeurG~%BX~^>5)_xqCEUEh>~aQBbQ%)&Ts2gu(hZp+EKRf>%`opE
z*rmwaJt+>Mnv1~lc@PIm0c7WFc2l$3h0%AxBnqzgoF!)#MxJ8YmbTp&<@5YFFG3`n
zRK)lKO;%E~Gd#h`ByiHOT05kr608$S{`H=nP8or#9#R1(yyX(?t)HXo<_Q?L9UaSu
z%VWW5L6SQ5+_`r{T}8_(05a^QeSC0DhQ=4=d@hDHk$`fyPl6_l6ZA%!QeyK~R$5X<@EBPs
z6mr#>@yPjP8Aes{u7xe2wZqgBavJ&=D0JT;%Au~&D1+|+O|2bK=&FF;;1CCNjvM6Y2rA1#PB>ldCu^Ct^?5
zjC#04phl(9-(H_Sde@MxP!O|LS0mcfLz5Vu1n6OmnD)X=iqC@>x(L#NvCQ-qo09hM
zf0;W)QMG_V`AHR1FjhM=`sw$yR-1(C=)?~$j|$*5_7@oysinfSvsF@)5mlV-Jwt?`
zO4MsO^fJy-0uS1*DCG15#`uDLhk5*WF${jJCX$_&vpLZij=-9%Qx{$B1AFBF6n&B9
zvc%BGkJ?v>?M=E|^in=s&8#&xM7x$6#DrASZ_h2tJ!)$teX+Y?t<#m^3PNsCCZSvl
zDeIF`JQ2BJt(EA7qaK%&SuoL58S%<(wlzfRf3n|t
z!#tm7n;`Nka^>_JNY&)=i1Q9d*Jj$i}O2Ib|LL}gk)s3
zZAKLWg#nkq>onjIOpnBUE3gGjyq5YPf|9!OI>C>E9Df}8=GFeSJv{kU1@|1{tDB~c9FCD4zW$$KV*<%O!``!3xt
zBX2aSMkfYlXYO$QF!(&hV%3;zU!%?Vk(GI!MolHcs-63phqvybJKhd*jeJ%PyIz=F
zxl+8=`GMP2u;N(R)O=P0)A;8^Q&6e<29GZHmKI=`GBo}gxcCa&Um`VpXjpa1J;gdm
zBE_l24tcG0WX;7R(gt5Sau=sIsJau`N+Y1}V#T*WntsL4$^nCO2u+=xDKOFBsl&q+c*?onp_Ig$2xVgMqVTsR
zA~SQu=?fQ((O*cZ6rfBN(b8z7z5Ob
zv;^yp+3g+pGEyAGPyJ)Bz@&f##wb~8Fkugu`
zSVE>qA?d=z3+dg3yv`Sxd4dc`a_J^$^43+TWMmub-`(I^rrOr@-gO2y=o#XyoE=$`zXVKEqG0;D@1eH2^^P95q-@m
z?VTX;b!IM1ni4IdgWjXef=wpy^j>w@BQc$JKFrzU!2I|uQwI&U9a=5?#{Z;}D|nE>
zIP4KYBYTc?FZ}@HV5e62A+-3=mfm&Ctgb8v4w`Ja3mA8_fF3n<9?8vMC
z64R^afvmMB*F>Q;Q&+5DK3s2q8H8DB1hM`ukk+R+J^{8I>558d4%IB-C8ca4y`8_?B3
z>Dl+_wKI%`l{_G_My6lTc|h#N>N=pIC(K{@Q!qEhZlw8EAD^>
zZT8CnaH6nr&{z*|R^kiBQ+F~Xc#k)Zy@qx=9X;1owIH&e11>e87B%t5{HdoXS2-to
zr#8ZScpc~&xA-Rzj|7=3lm?zNxvYzQFKs}`i7PHh*@@1f#4bY=*JAGO$B`KswU6qk
z4pv|D(EcXX?%Kgk61Rp87zrFONG1qP5O2PD5;%um4G&L#&VdIksYvht7=x@A%-0})
z#M0klAu-FByr+33Z>(rhsl9r{hKy(dGIb0NO@Upvu
zF|hY1SW4V1-iLMxtBVWRX{0+t#+3TzYB|i!p-tu;%KcE(?OVhTZAcDp3R2|PDiGg}
zmQ!0&b4?b@50a{16wL8ddl(&o4ZNGif}F!QvwPq4tjrRx^QQg=y*}#STZ+-TgYO3N
z>n8i!y?A01eJmuz7c+$#v1s_y%&Dw0zk9lwW>rQ>9Mj!Qi}%{U!*_vy4|JDp04i(n
zH?m{#z{^5aANtNPZ6C!y^sYAVmnHpn$N+j6S;F=gHKtH^yZ|^Au9_5R*O_?Qj;zem
zxaZs0$F~cllF440*kOy9khe)tt|^BDNJ8Tcu{-DD>$IZ_Kc|8u4!r56T3aoqK?q06
zi?@8kkW6&x!?7cg@NoQyCY%D##F^$x2}
zmJs7q`ZtUjtlKHFFz4p;aWhHjF6Um@rktsn?oI1D*-Hd!(Df5N>jAUh#W*oc<&Bi7
zHo%dei>%VPt3hk6MxNi_Es7TtGMdasl<|~f(O$o~A?FarcW*)Fe*vwclnHDZ?}+SP
zgYR&kn8RYb9O9><=RQuodIuwAN)ulS;SGy)PX}i^~1UYf0k$b`JnqicQgxToaq?rolg6VA7$>(o?TbE
z6jEI3BBqxbUgL{6t@896dly!z7dXPugQXkT$}T@PWqm_3(f}$Agk`G%;50L{=*mi|
zTE(qgB%svciozkc)BYou have the following attributes:
+
+
+ Name Values
+
+
+ {% for attr in attributes %}
+ {{ attr.0 }}
+
+ {% for val in attr.1 %}
+ {{ val }}
+ {% endfor %}
+
+ {% endfor %}
+
+
+ {% else %}
+ You don't have any attributes
+ {% endif %}
+ Logout
+{% else %}
+ Login and access again to this page
+{% endif %}
+
+{% endblock %}
diff --git a/demo_pyramid/demo_pyramid/templates/index.jinja2 b/demo_pyramid/demo_pyramid/templates/index.jinja2
new file mode 100644
index 00000000..65bcedd6
--- /dev/null
+++ b/demo_pyramid/demo_pyramid/templates/index.jinja2
@@ -0,0 +1,55 @@
+{% extends "layout.jinja2" %}
+
+{% block content %}
+
+
+
Pyramid Starter project
+
Welcome to demo_pyramid , a Pyramid application generated byCookiecutter .
+
+
+{% if errors %}
+
+
Errors:
+
+ {% for err in errors %}
+ {{err}}
+ {% endfor %}
+ Reason: {{ error_reason }}
+
+
+{% endif %}
+
+{% if not_auth_warn %}
+ Not authenticated
+{% endif %}
+
+{% if success_slo %}
+ Successfully logged out
+{% endif %}
+
+{% if paint_logout %}
+ {% if attributes %}
+
+
+ Name Values
+
+
+ {% for attr in attributes %}
+ {{ attr.0 }}
+
+ {% for val in attr.1 %}
+ {{ val }}
+ {% endfor %}
+
+ {% endfor %}
+
+
+ {% else %}
+ You don't have any attributes
+ {% endif %}
+ Logout
+{% else %}
+ Login Login and access to attrs page
+{% endif %}
+
+{% endblock %}
diff --git a/demo_pyramid/demo_pyramid/templates/layout.jinja2 b/demo_pyramid/demo_pyramid/templates/layout.jinja2
new file mode 100644
index 00000000..57ad87c0
--- /dev/null
+++ b/demo_pyramid/demo_pyramid/templates/layout.jinja2
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+ Cookiecutter Starter project for the Pyramid Web Framework
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {% block content %}
+
No content
+ {% endblock content %}
+
+
+
+
+
+ Copyright © Pylons Project
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/demo_pyramid/demo_pyramid/views.py b/demo_pyramid/demo_pyramid/views.py
new file mode 100644
index 00000000..86814a99
--- /dev/null
+++ b/demo_pyramid/demo_pyramid/views.py
@@ -0,0 +1,126 @@
+import os
+
+from pyramid.httpexceptions import (HTTPFound, HTTPInternalServerError, HTTPOk,)
+from pyramid.view import view_config
+
+from onelogin.saml2.auth import OneLogin_Saml2_Auth
+from onelogin.saml2.utils import OneLogin_Saml2_Utils
+
+SAML_PATH = os.path.join(os.path.dirname(__file__), 'saml')
+
+
+def init_saml_auth(req):
+ auth = OneLogin_Saml2_Auth(req, custom_base_path=SAML_PATH)
+ return auth
+
+
+def prepare_pyramid_request(request):
+ # If server is behind proxys or balancers use the HTTP_X_FORWARDED fields
+ return {
+ 'https': 'on' if request.scheme == 'https' else 'off',
+ 'http_host': request.host,
+ 'server_port': request.server_port,
+ 'script_name': request.path,
+ 'get_data': request.GET.copy(),
+ # Uncomment if using ADFS as IdP, https://github.com/onelogin/python-saml/pull/144
+ # 'lowercase_urlencoding': True,
+ 'post_data': request.POST.copy(),
+ }
+
+
+@view_config(route_name='index', renderer='templates/index.jinja2')
+def index(request):
+ req = prepare_pyramid_request(request)
+ auth = init_saml_auth(req)
+ errors = []
+ error_reason = ""
+ not_auth_warn = False
+ success_slo = False
+ attributes = False
+ paint_logout = False
+
+ session = request.session
+
+ if 'sso' in request.GET:
+ return HTTPFound(auth.login())
+ elif 'sso2' in request.GET:
+ return_to = '%s/attrs/' % request.host_url
+ return HTTPFound(auth.login(return_to))
+ elif 'slo' in request.GET:
+ name_id = None
+ session_index = None
+ if 'samlNameId' in session:
+ name_id = session['samlNameId']
+ if 'samlSessionIndex' in session:
+ session_index = session['samlSessionIndex']
+
+ return HTTPFound(auth.logout(name_id=name_id, session_index=session_index))
+ elif 'acs' in request.GET:
+ auth.process_response()
+ errors = auth.get_errors()
+ not_auth_warn = not auth.is_authenticated()
+ if len(errors) == 0:
+ session['samlUserdata'] = auth.get_attributes()
+ session['samlNameId'] = auth.get_nameid()
+ session['samlSessionIndex'] = auth.get_session_index()
+ self_url = OneLogin_Saml2_Utils.get_self_url(req)
+ if 'RelayState' in request.POST and self_url != request.POST['RelayState']:
+ return HTTPFound(auth.redirect_to(request.POST['RelayState']))
+ else:
+ error_reason = auth.get_last_error_reason()
+ elif 'sls' in request.GET:
+ dscb = lambda: session.clear()
+ url = auth.process_slo(delete_session_cb=dscb)
+ errors = auth.get_errors()
+ if len(errors) == 0:
+ if url is not None:
+ return HTTPFound(url)
+ else:
+ success_slo = True
+
+ if 'samlUserdata' in session:
+ paint_logout = True
+ if len(session['samlUserdata']) > 0:
+ attributes = session['samlUserdata'].items()
+
+ return {
+ 'errors': errors,
+ 'error_reason': error_reason,
+ 'not_auth_warn': not_auth_warn,
+ 'success_slo': success_slo,
+ 'attributes': attributes,
+ 'paint_logout': paint_logout,
+ }
+
+
+@view_config(route_name='attrs', renderer='templates/attrs.jinja2')
+def attrs(request):
+ paint_logout = False
+ attributes = False
+
+ session = request.session
+
+ if 'samlUserdata' in session:
+ paint_logout = True
+ if len(session['samlUserdata']) > 0:
+ attributes = session['samlUserdata'].items()
+
+ return {
+ 'paint_logout': paint_logout,
+ 'attributes': attributes,
+ }
+
+
+@view_config(route_name='metadata', renderer='html')
+def metadata(request):
+ req = prepare_pyramid_request(request)
+ auth = init_saml_auth(req)
+ settings = auth.get_settings()
+ metadata = settings.get_sp_metadata()
+ errors = settings.validate_metadata(metadata)
+
+ if len(errors) == 0:
+ resp = HTTPOk(body=metadata, headers={'Content-Type': 'text/xml'})
+ else:
+ resp = HTTPInternalServerError(body=', '.join(errors))
+ return resp
diff --git a/demo_pyramid/development.ini b/demo_pyramid/development.ini
new file mode 100644
index 00000000..64042ee7
--- /dev/null
+++ b/demo_pyramid/development.ini
@@ -0,0 +1,59 @@
+###
+# app configuration
+# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
+###
+
+[app:main]
+use = egg:demo_pyramid
+
+pyramid.reload_templates = true
+pyramid.debug_authorization = false
+pyramid.debug_notfound = false
+pyramid.debug_routematch = false
+pyramid.default_locale_name = en
+pyramid.includes =
+ pyramid_debugtoolbar
+
+# By default, the toolbar only appears for clients from IP addresses
+# '127.0.0.1' and '::1'.
+# debugtoolbar.hosts = 127.0.0.1 ::1
+
+###
+# wsgi server configuration
+###
+
+[server:main]
+use = egg:waitress#main
+listen = 127.0.0.1:6543 [::1]:6543
+
+###
+# logging configuration
+# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
+###
+
+[loggers]
+keys = root, demo_pyramid
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = INFO
+handlers = console
+
+[logger_demo_pyramid]
+level = DEBUG
+handlers =
+qualname = demo_pyramid
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
diff --git a/demo_pyramid/production.ini b/demo_pyramid/production.ini
new file mode 100644
index 00000000..84b482ae
--- /dev/null
+++ b/demo_pyramid/production.ini
@@ -0,0 +1,53 @@
+###
+# app configuration
+# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/environment.html
+###
+
+[app:main]
+use = egg:demo_pyramid
+
+pyramid.reload_templates = false
+pyramid.debug_authorization = false
+pyramid.debug_notfound = false
+pyramid.debug_routematch = false
+pyramid.default_locale_name = en
+
+###
+# wsgi server configuration
+###
+
+[server:main]
+use = egg:waitress#main
+listen = *:6543
+
+###
+# logging configuration
+# http://docs.pylonsproject.org/projects/pyramid/en/latest/narr/logging.html
+###
+
+[loggers]
+keys = root, demo_pyramid
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = WARN
+handlers = console
+
+[logger_demo_pyramid]
+level = WARN
+handlers =
+qualname = demo_pyramid
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(asctime)s %(levelname)-5.5s [%(name)s:%(lineno)s][%(threadName)s] %(message)s
diff --git a/demo_pyramid/setup.py b/demo_pyramid/setup.py
new file mode 100644
index 00000000..a30a8ddd
--- /dev/null
+++ b/demo_pyramid/setup.py
@@ -0,0 +1,45 @@
+import os
+
+from setuptools import setup, find_packages
+
+here = os.path.abspath(os.path.dirname(__file__))
+with open(os.path.join(here, 'README.txt')) as f:
+ README = f.read()
+with open(os.path.join(here, 'CHANGES.txt')) as f:
+ CHANGES = f.read()
+
+requires = [
+ 'pyramid',
+ 'pyramid_jinja2',
+ 'pyramid_debugtoolbar',
+ 'waitress',
+ 'xmlsec',
+ 'isodate',
+ 'python-saml',
+]
+
+setup(
+ name='demo_pyramid',
+ version='0.0',
+ description='demo_pyramid',
+ long_description=README + '\n\n' + CHANGES,
+ classifiers=[
+ 'Programming Language :: Python',
+ 'Framework :: Pyramid',
+ 'Topic :: Internet :: WWW/HTTP',
+ 'Topic :: Internet :: WWW/HTTP :: WSGI :: Application',
+ ],
+ author='',
+ author_email='',
+ url='',
+ keywords='web pyramid pylons',
+ packages=find_packages(),
+ include_package_data=True,
+ zip_safe=False,
+ install_requires=requires,
+ entry_points={
+ 'paste.app_factory': [
+ 'main = demo_pyramid:main',
+ ],
+ },
+)
From 2b5091c91c5d8b3841bea66953d22ae974109b8a Mon Sep 17 00:00:00 2001
From: adam
Date: Fri, 21 Apr 2017 18:11:07 +1000
Subject: [PATCH 094/255] #190 Checking the status of response before assertion
count Failed Responses don't have assertions and the error hides that the
status is not success
---
src/onelogin/saml2/response.py | 6 +++---
tests/src/OneLogin/saml2_tests/response_test.py | 11 +++++++++++
2 files changed, 14 insertions(+), 3 deletions(-)
diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py
index 93f650a3..ba524c2b 100644
--- a/src/onelogin/saml2/response.py
+++ b/src/onelogin/saml2/response.py
@@ -84,6 +84,9 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False):
OneLogin_Saml2_ValidationError.MISSING_ID
)
+ # Checks that the response has the SUCCESS status
+ self.check_status()
+
# Checks that the response only has one assertion
if not self.validate_num_assertions():
raise OneLogin_Saml2_ValidationError(
@@ -91,9 +94,6 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False):
OneLogin_Saml2_ValidationError.WRONG_NUMBER_OF_ASSERTIONS
)
- # Checks that the response has the SUCCESS status
- self.check_status()
-
idp_data = self.__settings.get_idp_data()
idp_entity_id = idp_data.get('entityId', '')
sp_data = self.__settings.get_sp_data()
diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py
index 8c499e9e..cea605d1 100644
--- a/tests/src/OneLogin/saml2_tests/response_test.py
+++ b/tests/src/OneLogin/saml2_tests/response_test.py
@@ -1391,6 +1391,17 @@ def testIsValidWithoutInResponseTo(self):
}))
+ def testStatusCheckBeforeAssertionCheck(self):
+ """
+ Tests the status of a response is checked before the assertion count. As failed statuses will have no assertions
+ """
+ settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
+ xml_2 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'status_code_responder.xml.base64'))
+ response_2 = OneLogin_Saml2_Response(settings, xml_2)
+ with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'The status code of the Response was not Success, was Responder'):
+ response_2.is_valid(self.get_request_data(), raise_exceptions=True)
+
+
if __name__ == '__main__':
if is_running_under_teamcity():
runner = TeamcityTestRunner()
From b670d9e091fc85b8b9da7d590fe8a4574c7e55a8 Mon Sep 17 00:00:00 2001
From: adam
Date: Fri, 21 Apr 2017 18:11:07 +1000
Subject: [PATCH 095/255] #190 Checking the status of response before assertion
count Failed Responses don't have assertions and the error hides that the
status is not success
---
tests/src/OneLogin/saml2_tests/response_test.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py
index cea605d1..7e040ec5 100644
--- a/tests/src/OneLogin/saml2_tests/response_test.py
+++ b/tests/src/OneLogin/saml2_tests/response_test.py
@@ -1396,10 +1396,10 @@ def testStatusCheckBeforeAssertionCheck(self):
Tests the status of a response is checked before the assertion count. As failed statuses will have no assertions
"""
settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
- xml_2 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'status_code_responder.xml.base64'))
- response_2 = OneLogin_Saml2_Response(settings, xml_2)
+ xml = self.file_contents(join(self.data_path, 'responses', 'invalids', 'status_code_responder.xml.base64'))
+ response = OneLogin_Saml2_Response(settings, xml)
with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'The status code of the Response was not Success, was Responder'):
- response_2.is_valid(self.get_request_data(), raise_exceptions=True)
+ response.is_valid(self.get_request_data(), raise_exceptions=True)
if __name__ == '__main__':
From ba9fdfee8729e6f040be07e28c5efc037c8e4f13 Mon Sep 17 00:00:00 2001
From: adam
Date: Fri, 21 Apr 2017 18:24:09 +1000
Subject: [PATCH 096/255] #190 Complying to pep8
---
tests/src/OneLogin/saml2_tests/response_test.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py
index 7e040ec5..d7ac4673 100644
--- a/tests/src/OneLogin/saml2_tests/response_test.py
+++ b/tests/src/OneLogin/saml2_tests/response_test.py
@@ -1390,7 +1390,6 @@ def testIsValidWithoutInResponseTo(self):
'script_name': 'newonelogin/demo1/index.php?acs'
}))
-
def testStatusCheckBeforeAssertionCheck(self):
"""
Tests the status of a response is checked before the assertion count. As failed statuses will have no assertions
From a9d16fdd0cf0984f744a6f77f814e77e6a9f5834 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Wed, 3 May 2017 10:02:47 +0200
Subject: [PATCH 097/255] Fix Readme titles
---
README.md | 52 ++++++++++++++++++++++++++--------------------------
1 file changed, 26 insertions(+), 26 deletions(-)
diff --git a/README.md b/README.md
index 275dd752..8a8f7786 100644
--- a/README.md
+++ b/README.md
@@ -144,7 +144,7 @@ the classes and methods that are described in a later section.
This folder contains a Django project that will be used as demo to show how to add SAML support to the Django Framework. 'demo' is the main folder of the django project (with its settings.py, views.py, urls.py), 'templates' is the django templates of the project and 'saml' is a folder that contains the 'certs' folder that could be used to store the x509 public and private key, and the saml toolkit settings (settings.json and advanced_settings.json).
-***Notice about certs***
+*** Notice about certs ***
SAML requires a x.509 cert to sign and encrypt elements like NameID, Message, Assertion, Metadata.
@@ -572,7 +572,7 @@ auth.get_last_request_id()
Related to the SP there are 3 important endpoints: The metadata view, the ACS view and the SLS view.
The toolkit provides examples of those views in the demos, but lets see an example.
-***SP Metadata***
+*** SP Metadata ***
This code will provide the XML metadata file of our SP, based on the info that we provided in the settings files.
@@ -598,7 +598,7 @@ saml_settings = OneLogin_Saml2_Settings(settings=None, custom_base_path=None, sp
```
to get the settings object and with the sp_validation_only=True parameter we will avoid the IdP Settings validation.
-***Attribute Consumer Service(ACS)***
+*** Attribute Consumer Service(ACS) ***
This code handles the SAML response that the IdP forwards to the SP through the user's client.
@@ -664,7 +664,7 @@ print auth.get_attribute('cn')
Before trying to get an attribute, check that the user is authenticated. If the user isn't authenticated, an empty dict will be returned. For example, if we call to get_attributes before a auth.process_response, the get_attributes() will return an empty dict.
-***Single Logout Service (SLS)***
+*** Single Logout Service (SLS) ***
This code handles the Logout Request and the Logout Responses.
@@ -765,7 +765,7 @@ If a match on the LogoutResponse ID and the LogoutRequest ID to be sent is requi
auth.get_last_request_id()
```
-####Example of a view that initiates the SSO request and handles the response (is the acs target)####
+#### Example of a view that initiates the SSO request and handles the response (is the acs target) ####
We can code a unique file that initiates the SSO process, handle the response, get the attributes, initiate the slo and processes the logout response.
@@ -820,7 +820,7 @@ else:
Described below are the main classes and methods that can be invoked from the SAML2 library.
-####OneLogin_Saml2_Auth - auth.py####
+#### OneLogin_Saml2_Auth - auth.py ####
Main class of OneLogin Python Toolkit
@@ -848,7 +848,7 @@ Main class of OneLogin Python Toolkit
* ***get_last_request_xml*** Returns the most recently-constructed/processed XML SAML request (AuthNRequest, LogoutRequest)
* ***get_last_response_xml*** Returns the most recently-constructed/processed XML SAML response (SAMLResponse, LogoutResponse). If the SAMLResponse had an encrypted assertion, decrypts it.
-####OneLogin_Saml2_Auth - authn_request.py####
+#### OneLogin_Saml2_Auth - authn_request.py ####
SAML 2 Authentication Request class
@@ -857,7 +857,7 @@ SAML 2 Authentication Request class
* ***get_id*** Returns the AuthNRequest ID.
* ***get_xml*** Returns the XML that will be sent as part of the request.
-####OneLogin_Saml2_Response - response.py####
+#### OneLogin_Saml2_Response - response.py ####
SAML 2 Authentication Response class
@@ -876,7 +876,7 @@ SAML 2 Authentication Response class
* ***get_error*** After execute a validation process, if fails this method returns the cause
* ***get_xml_document*** Returns the SAML Response document (If contains an encrypted assertion, decrypts it).
-####OneLogin_Saml2_LogoutRequest - logout_request.py####
+#### OneLogin_Saml2_LogoutRequest - logout_request.py ####
SAML 2 Logout Request class
@@ -891,7 +891,7 @@ SAML 2 Logout Request class
* ***get_error*** After execute a validation process, if fails this method returns the cause.
* ***get_xml*** Returns the XML that will be sent as part of the request or that was received at the SP
-####OneLogin_Saml2_LogoutResponse - logout_response.py####
+#### OneLogin_Saml2_LogoutResponse - logout_response.py ####
SAML 2 Logout Response class
@@ -905,7 +905,7 @@ SAML 2 Logout Response class
* ***get_xml*** Returns the XML that will be sent as part of the response or that was received at the SP
-####OneLogin_Saml2_Settings - settings.py####
+#### OneLogin_Saml2_Settings - settings.py ####
Configuration of the OneLogin Python Toolkit
@@ -937,7 +937,7 @@ Configuration of the OneLogin Python Toolkit
* ***is_strict*** Returns if the 'strict' mode is active.
* ***is_debug_active*** Returns if the debug is active.
-####OneLogin_Saml2_Metadata - metadata.py####
+#### OneLogin_Saml2_Metadata - metadata.py ####
A class that contains functionality related to the metadata of the SP
@@ -945,7 +945,7 @@ A class that contains functionality related to the metadata of the SP
* ***sign_metadata*** Signs the metadata with the key/cert provided.
* ***add_x509_key_descriptors*** Adds the x509 descriptors (sign/encriptation) to the metadata
-####OneLogin_Saml2_Utils - utils.py####
+#### OneLogin_Saml2_Utils - utils.py ####
Auxiliary class that contains several methods
@@ -981,7 +981,7 @@ Auxiliary class that contains several methods
* ***def get_encoded_parameter*** Return an url encoded get parameter value
* ***extract_raw_query_parameter***
-####OneLogin_Saml2_IdPMetadataParser - idp_metadata_parser.py####
+#### OneLogin_Saml2_IdPMetadataParser - idp_metadata_parser.py ####
A class that contains methods to obtain and parse metadata from IdP
@@ -1006,7 +1006,7 @@ how it deployed. New demos using other python frameworks are welcome as a contri
We said that this toolkit includes a django application demo and a flask applicacion demo,
lets see how fast is deploy them.
-***Virtualenv***
+*** Virtualenv ***
The use of a [virtualenv](http://virtualenv.readthedocs.org/en/latest/) is
highly recommended.
@@ -1051,7 +1051,7 @@ Now, with the virtualenv loaded, you can run the demo like this:
You'll have the demo running at http://localhost:8000
-####Content####
+#### Content ####
The flask project contains:
@@ -1063,7 +1063,7 @@ The flask project contains:
* ***saml*** Is a folder that contains the 'certs' folder that could be used to store the x509 public and private key, and the saml toolkit settings (settings.json and advanced_settings.json).
-####SP setup####
+#### SP setup ####
The Onelogin's Python Toolkit allows you to provide the settings info in 2 ways: settings files or define a setting dict. In the demo-flask it used the first method.
@@ -1071,11 +1071,11 @@ In the index.py file we define the app.config['SAML_PATH'], that will target to
First we need to edit the saml/settings.json, configure the SP part and review the metadata of the IdP and complete the IdP info. Later edit the saml/advanced_settings.json files and configure the how the toolkit will work. Check the settings section of this document if you have any doubt.
-####IdP setup####
+#### IdP setup ####
Once the SP is configured, the metadata of the SP is published at the /metadata url. Based on that info, configure the IdP.
-####How it works####
+#### How it works ####
1. First time you access to the main view 'http://localhost:8000', you can select to login and return to the same view or login and be redirected to /?attrs (attrs view).
@@ -1120,7 +1120,7 @@ Note that many of the configuration files expect HTTPS. This is not required by
If you want to integrate a production django application, take a look on this SAMLServiceProviderBackend that uses our toolkit to add SAML support: https://github.com/KristianOellegaard/django-saml-service-provider
-####Content####
+#### Content ####
The django project contains:
@@ -1136,7 +1136,7 @@ The django project contains:
* ***templates***. Is the folder where django stores the templates of the project. It was implemented a base.html template that is extended by index.html and attrs.html, the templates of our simple demo that shows messages, user attributes when available and login and logout links.
-####SP setup####
+#### SP setup ####
The Onelogin's Python Toolkit allows you to provide the settings info in 2 ways: settings files or define a setting dict. In the demo-django it used the first method.
@@ -1144,11 +1144,11 @@ After set the SAML_FOLDER in the demo/settings.py, the settings of the python to
First we need to edit the saml/settings.json, configure the SP part and review the metadata of the IdP and complete the IdP info. Later edit the saml/advanced_settings.json files and configure the how the toolkit will work. Check the settings section of this document if you have any doubt.
-####IdP setup####
+#### IdP setup ####
Once the SP is configured, the metadata of the SP is published at the /metadata url. Based on that info, configure the IdP.
-####How it works####
+#### How it works ####
This demo works very similar to the flask-demo (We did it intentionally).
@@ -1177,7 +1177,7 @@ Now you can run the demo like this:
If that worked, the demo is now running at http://localhost:6543.
-####Content####
+#### Content ####
The Pyramid project contains:
@@ -1191,7 +1191,7 @@ The Pyramid project contains:
* ***saml*** is a folder that contains the 'certs' folder that could be used to store the x509 public and private key, and the saml toolkit settings (settings.json and advanced_settings.json).
-####SP setup####
+#### SP setup ####
The Onelogin's Python Toolkit allows you to provide the settings info in 2 ways: settings files or define a setting dict. In demo_pyramid the first method is used.
@@ -1203,7 +1203,7 @@ First we need to edit the saml/settings.json, configure the SP part and review t
Once the SP is configured, the metadata of the SP is published at the /metadata/ url. Based on that info, configure the IdP.
-####How it works####
+#### How it works ####
1. First time you access to the main view 'http://localhost:6543', you can select to login and return to the same view or login and be redirected to /?attrs (attrs view).
From 5c1d869c48e2fa89921ee83f352292c67f50149e Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Wed, 3 May 2017 10:03:37 +0200
Subject: [PATCH 098/255] Update README.md
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 8a8f7786..30d6c650 100644
--- a/README.md
+++ b/README.md
@@ -1199,7 +1199,7 @@ In the views.py file we define the SAML_PATH, which will target the 'saml' folde
First we need to edit the saml/settings.json, configure the SP part and review the metadata of the IdP and complete the IdP info. Later edit the saml/advanced_settings.json files and configure the how the toolkit will work. Check the settings section of this document if you have any doubt.
-####IdP setup####
+#### IdP setup ####
Once the SP is configured, the metadata of the SP is published at the /metadata/ url. Based on that info, configure the IdP.
From 5e17c19cbdf0833d15e7d60a85d1a6924ff6e519 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Tue, 9 May 2017 17:53:19 +0200
Subject: [PATCH 099/255] Be able to relax SSL Certificate verification when
retrieving idp metadata
---
src/onelogin/saml2/idp_metadata_parser.py | 15 +++++++++++----
.../OneLogin/saml2_tests/signed_response_test.py | 3 +++
2 files changed, 14 insertions(+), 4 deletions(-)
diff --git a/src/onelogin/saml2/idp_metadata_parser.py b/src/onelogin/saml2/idp_metadata_parser.py
index 80b38ec1..fc9689cb 100644
--- a/src/onelogin/saml2/idp_metadata_parser.py
+++ b/src/onelogin/saml2/idp_metadata_parser.py
@@ -10,6 +10,7 @@
"""
import urllib2
+import ssl
from copy import deepcopy
from defusedxml.lxml import fromstring
@@ -24,7 +25,7 @@ class OneLogin_Saml2_IdPMetadataParser(object):
"""
@staticmethod
- def get_metadata(url):
+ def get_metadata(url, validate_cert=True):
"""
Gets the metadata XML from the provided URL
@@ -35,7 +36,13 @@ def get_metadata(url):
:rtype: string
"""
valid = False
- response = urllib2.urlopen(url)
+ if validate_cert:
+ response = urllib2.urlopen(url)
+ else:
+ ctx = ssl.create_default_context()
+ ctx.check_hostname = False
+ ctx.verify_mode = ssl.CERT_NONE
+ response = urllib2.urlopen(url, context=ctx)
xml = response.read()
if xml:
@@ -53,7 +60,7 @@ def get_metadata(url):
return xml
@staticmethod
- def parse_remote(url, **kwargs):
+ def parse_remote(url, validate_cert=True, **kwargs):
"""
Gets the metadata XML from the provided URL and parse it, returning a dict with extracted data
@@ -63,7 +70,7 @@ def parse_remote(url, **kwargs):
:returns: settings dict with extracted data
:rtype: dict
"""
- idp_metadata = OneLogin_Saml2_IdPMetadataParser.get_metadata(url)
+ idp_metadata = OneLogin_Saml2_IdPMetadataParser.get_metadata(url, validate_cert)
return OneLogin_Saml2_IdPMetadataParser.parse(idp_metadata, **kwargs)
@staticmethod
diff --git a/tests/src/OneLogin/saml2_tests/signed_response_test.py b/tests/src/OneLogin/saml2_tests/signed_response_test.py
index d630f00f..50a4431f 100644
--- a/tests/src/OneLogin/saml2_tests/signed_response_test.py
+++ b/tests/src/OneLogin/saml2_tests/signed_response_test.py
@@ -44,6 +44,9 @@ def testResponseSignedAssertionNot(self):
response = OneLogin_Saml2_Response(settings, b64encode(message))
self.assertEquals('someone@example.org', response.get_nameid())
+ from onelogin.saml2.utils import OneLogin_Saml2_Utils
+ assertion_nodes = OneLogin_Saml2_Utils.query(response.document, '//saml:Assertion')
+ self.assertEquals(len(assertion_nodes), 1)
def testResponseAndAssertionSigned(self):
"""
From 37d6190367b1400fc2be73d73fc80f9944bda6b8 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Thu, 11 May 2017 12:52:11 +0200
Subject: [PATCH 100/255] Delete expensify_test.py.txt
---
.../saml2_tests/expensify_test.py.txt | 50 -------------------
1 file changed, 50 deletions(-)
delete mode 100644 tests/src/OneLogin/saml2_tests/expensify_test.py.txt
diff --git a/tests/src/OneLogin/saml2_tests/expensify_test.py.txt b/tests/src/OneLogin/saml2_tests/expensify_test.py.txt
deleted file mode 100644
index c02d164d..00000000
--- a/tests/src/OneLogin/saml2_tests/expensify_test.py.txt
+++ /dev/null
@@ -1,50 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright (c) 2014, OneLogin, Inc.
-# All rights reserved.
-
-from base64 import b64decode
-import json
-from lxml import etree
-from os.path import dirname, join, exists
-import unittest
-from teamcity import is_running_under_teamcity
-from teamcity.unittestpy import TeamcityTestRunner
-from xml.dom.minidom import Document, parseString
-
-from onelogin.saml2.constants import OneLogin_Saml2_Constants
-from onelogin.saml2.settings import OneLogin_Saml2_Settings
-from onelogin.saml2.utils import OneLogin_Saml2_Utils
-
-
-class OneLogin_Saml2_Utils_Test(unittest.TestCase):
- data_path = join(dirname(dirname(dirname(dirname(__file__)))), 'data')
-
- def file_contents(self, filename):
- f = open(filename, 'r')
- content = f.read()
- f.close()
- return content
-
-
- def testExpesify(self):
- xml = self.file_contents(join(self.data_path, 'responses', 'expensify.xml'))
- cert = """-----BEGIN CERTIFICATE-----
-MIIC5jCCAc6gAwIBAgIQejzcQ7LOo7NDphYfVqKB8jANBgkqhkiG9w0BAQsFADAv
-MS0wKwYDVQQDEyRBREZTIFNpZ25pbmcgLSBhZGZzLmZpbmlzaG1hc3Rlci5jb20w
-HhcNMTYwMzMwMjA1NjM2WhcNMTcwMzMwMjA1NjM2WjAvMS0wKwYDVQQDEyRBREZT
-IFNpZ25pbmcgLSBhZGZzLmZpbmlzaG1hc3Rlci5jb20wggEiMA0GCSqGSIb3DQEB
-AQUAA4IBDwAwggEKAoIBAQDrnGb3jm22UR0CqYQK55vj5dVMQIitpl5jUHuFgou0
-SsFfI5VJY5KNXQXCJoduXlxl12dRUQrd0h22TCLBWoneuDcor0UV4bRl7QyQWjuX
-M6pEDG8JsNWJEmnoiKGVmnfsaVgKTFZxO+Kydk/GRJauOqAyU7igABDpazMMHjjL
-c9iZ6tHjFjtWrUay3Hu3aaZheQWgzjnENUprgnH3zm5NQT5Dbd72z5TKrb4bbv08
-oInf52dnBNiSz0wa8uZRJPJJH2rGJmWUIJqp2fnhCxysMgf+ny5IfOkpcG4Cb/fk
-d4hoSmY5Bb/dKuD9/pTpmoylcinBtb3bMGASkulyQmENAgMBAAEwDQYJKoZIhvcN
-AQELBQADggEBAEx5l+gRoxUSY1nu6O2Cmu8WpnrFlEoNsko+Z/T34NYIm/2uaX6y
-kHoDSptgplwjtjE6lE0Zygm5R59BAU3tqGG1uHu4G7w++rcE6nnYunC3tZ8iKuzb
-VmkyUFjuf0AaWo/j/pl7zDJV1NHg6zm/NrqKhx4+Sr/LpVdBMOQmU9leqKsfVtkJ
-ktTOx1ChiEqu7R0yfKuaZwSlUfgnVtVS7yvLvk0KIbxhZe9BGdtJ03+XHYE0TeVN
-SpHh6Q1mylYurMX3WLMW5cQ+AcOWiDob2FuMq6wNWoUwOCvUwTu9NFQvuyzFpUrn
-wFgrDzL8lRc9nNA/iZh+pBIu+BM3/wWZLp8=
------END CERTIFICATE-----"""
- self.assertTrue(OneLogin_Saml2_Utils.validate_sign(xml, cert, debug=True))
From dc015e0ba469caabe6e9095f81cf2c71d5fe4847 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Thu, 11 May 2017 13:26:02 +0200
Subject: [PATCH 101/255] Be able to register future SP x509cert on the
settings and publish it on SP metadata
---
README.md | 14 ++++++
demo-bottle/saml/certs/README | 5 +-
demo-django/saml/certs/README | 5 +-
demo-flask/saml/certs/README | 5 +-
demo_pyramid/demo_pyramid/saml/certs/README | 5 +-
src/onelogin/saml2/settings.py | 29 +++++++++++
tests/settings/settings7.json | 50 +++++++++++++++++++
.../saml2_tests/expensify_test.py.txt | 50 -------------------
.../src/OneLogin/saml2_tests/settings_test.py | 32 ++++++++++++
.../saml2_tests/signed_response_test.py | 3 --
10 files changed, 137 insertions(+), 61 deletions(-)
create mode 100644 tests/settings/settings7.json
delete mode 100644 tests/src/OneLogin/saml2_tests/expensify_test.py.txt
diff --git a/README.md b/README.md
index 30d6c650..de0210f8 100644
--- a/README.md
+++ b/README.md
@@ -157,6 +157,9 @@ Or also we can provide those data in the setting file at the 'x509cert' and the
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.
+Use `sp_new.crt` if you are in a key rollover process and you want to
+publish that x509certificate 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
@@ -279,6 +282,15 @@ This is the settings.json file:
// the certs folder. But we can also provide them with the following parameters
"x509cert": "",
"privateKey": ""
+
+ /*
+ * Key rollover
+ * If you plan to update the SP x509cert and privateKey
+ * you can define here the new x509cert and it will be
+ * published on the SP metadata so Identity Providers can
+ * read them and get ready for rollover.
+ */
+ // 'x509certNew': '',
},
// Identity Provider Data that we want connected with our SP.
@@ -924,6 +936,7 @@ Configuration of the OneLogin Python Toolkit
* ***check_sp_certs*** Checks if the x509 certs of the SP exists and are valid.
* ***get_sp_key*** Returns the x509 private key of the SP.
* ***get_sp_cert*** Returns the x509 public cert of the SP.
+* ***get_sp_cert_new*** Returns the future x509 public cert of the SP.
* ***get_idp_cert*** Returns the x509 public cert of the IdP.
* ***get_sp_data*** Gets the SP data.
* ***get_idp_data*** Gets the IdP data.
@@ -932,6 +945,7 @@ Configuration of the OneLogin Python Toolkit
* ***get_organization*** Gets organization data.
* ***format_idp_cert*** Formats the IdP cert.
* ***format_sp_cert*** Formats the SP cert.
+* ***format_sp_cert_new*** Formats the SP cert new.
* ***format_sp_key*** Formats the private key.
* ***set_strict*** Activates or deactivates the strict mode.
* ***is_strict*** Returns if the 'strict' mode is active.
diff --git a/demo-bottle/saml/certs/README b/demo-bottle/saml/certs/README
index 03c13737..7cf0c143 100644
--- a/demo-bottle/saml/certs/README
+++ b/demo-bottle/saml/certs/README
@@ -2,8 +2,9 @@ Take care of this folder that could contain private key. Be sure that this folde
Onelogin Python Toolkit expects that certs for the SP could be stored in this folder as:
- * sp.key Private Key
- * sp.crt Public cert
+ * sp.key Private Key
+ * sp.crt Public cert
+ * sp_new.crt Future Public cert
Also you can use other cert to sign the metadata of the SP using the:
diff --git a/demo-django/saml/certs/README b/demo-django/saml/certs/README
index 03c13737..7cf0c143 100644
--- a/demo-django/saml/certs/README
+++ b/demo-django/saml/certs/README
@@ -2,8 +2,9 @@ Take care of this folder that could contain private key. Be sure that this folde
Onelogin Python Toolkit expects that certs for the SP could be stored in this folder as:
- * sp.key Private Key
- * sp.crt Public cert
+ * sp.key Private Key
+ * sp.crt Public cert
+ * sp_new.crt Future Public cert
Also you can use other cert to sign the metadata of the SP using the:
diff --git a/demo-flask/saml/certs/README b/demo-flask/saml/certs/README
index 03c13737..7cf0c143 100644
--- a/demo-flask/saml/certs/README
+++ b/demo-flask/saml/certs/README
@@ -2,8 +2,9 @@ Take care of this folder that could contain private key. Be sure that this folde
Onelogin Python Toolkit expects that certs for the SP could be stored in this folder as:
- * sp.key Private Key
- * sp.crt Public cert
+ * sp.key Private Key
+ * sp.crt Public cert
+ * sp_new.crt Future Public cert
Also you can use other cert to sign the metadata of the SP using the:
diff --git a/demo_pyramid/demo_pyramid/saml/certs/README b/demo_pyramid/demo_pyramid/saml/certs/README
index 03c13737..7cf0c143 100644
--- a/demo_pyramid/demo_pyramid/saml/certs/README
+++ b/demo_pyramid/demo_pyramid/saml/certs/README
@@ -2,8 +2,9 @@ Take care of this folder that could contain private key. Be sure that this folde
Onelogin Python Toolkit expects that certs for the SP could be stored in this folder as:
- * sp.key Private Key
- * sp.crt Public cert
+ * sp.key Private Key
+ * sp.crt Public cert
+ * sp_new.crt Future Public cert
Also you can use other cert to sign the metadata of the SP using the:
diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py
index 1c17ec3e..8322eaf0 100644
--- a/src/onelogin/saml2/settings.py
+++ b/src/onelogin/saml2/settings.py
@@ -111,6 +111,8 @@ def __init__(self, settings=None, custom_base_path=None, sp_validation_only=Fals
self.format_idp_cert()
self.format_sp_cert()
+ if 'x509certNew' in self.__sp:
+ self.format_sp_cert_new()
self.format_sp_key()
def __load_paths(self, base_path=None):
@@ -522,6 +524,23 @@ def get_sp_cert(self):
return cert or None
+ def get_sp_cert_new(self):
+ """
+ Returns the x509 public of the SP planned
+ to be used soon instead the other public cert
+
+ :returns: SP public cert new
+ :rtype: string or None
+ """
+ cert = self.__sp.get('x509certNew')
+ cert_file_name = self.__paths['cert'] + 'sp_new.crt'
+
+ if not cert and exists(cert_file_name):
+ with open(cert_file_name) as f:
+ cert = f.read()
+
+ return cert or None
+
def get_idp_cert(self):
"""
Returns the x509 public cert of the IdP.
@@ -590,6 +609,10 @@ def get_sp_metadata(self):
self.__security['metadataCacheDuration'],
self.get_contacts(), self.get_organization()
)
+
+ cert_new = self.get_sp_cert_new()
+ metadata = OneLogin_Saml2_Metadata.add_x509_key_descriptors(metadata, cert_new)
+
cert = self.get_sp_cert()
metadata = OneLogin_Saml2_Metadata.add_x509_key_descriptors(metadata, cert)
@@ -705,6 +728,12 @@ def format_sp_cert(self):
"""
self.__sp['x509cert'] = OneLogin_Saml2_Utils.format_cert(self.__sp['x509cert'])
+ def format_sp_cert_new(self):
+ """
+ Formats the SP cert.
+ """
+ self.__sp['x509certNew'] = OneLogin_Saml2_Utils.format_cert(self.__sp['x509certNew'])
+
def format_sp_key(self):
"""
Formats the private key.
diff --git a/tests/settings/settings7.json b/tests/settings/settings7.json
new file mode 100644
index 00000000..e573624b
--- /dev/null
+++ b/tests/settings/settings7.json
@@ -0,0 +1,50 @@
+{
+ "strict": false,
+ "debug": false,
+ "custom_base_path": "../../../tests/data/customPath/",
+ "sp": {
+ "entityId": "http://stuff.com/endpoints/metadata.php",
+ "assertionConsumerService": {
+ "url": "http://stuff.com/endpoints/endpoints/acs.php"
+ },
+ "singleLogoutService": {
+ "url": "http://stuff.com/endpoints/endpoints/sls.php"
+ },
+ "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified",
+ "privateKey": "MIICXgIBAAKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABAoGAD4/Z4LWVWV6D1qMIp1Gzr0ZmdWTE1SPdZ7Ej8glGnCzPdguCPuzbhGXmIg0VJ5D+02wsqws1zd48JSMXXM8zkYZVwQYIPUsNn5FetQpwxDIMPmhHg+QNBgwOnk8JK2sIjjLPL7qY7Itv7LT7Gvm5qSOkZ33RCgXcgz+okEIQMYkCQQDzbTOyDL0c5WQV6A2k06T/azdhUdGXF9C0+WkWSfNaovmTgRXh1G+jMlr82Snz4p4/STt7P/XtyWzF3pkVgZr3AkEA7nPjXwHlttNEMo6AtxHd47nizK2NUN803ElIUT8P9KSCoERmSXq66PDekGNic4ldpsSvOeYCk8MAYoDBy9kvVwJBAMLgX4xg6lzhv7hR5+pWjTb1rIY6rCHbrPfU264+UZXz9v2BT/VUznLF81WMvStD9xAPHpFS6R0OLghSZhdzhI0CQQDL8Duvfxzrn4b9QlmduV8wLERoT6rEVxKLsPVz316TGrxJvBZLk/cV0SRZE1cZf4ukXSWMfEcJ/0Zt+LdG1CqjAkEAqwLSglJ9Dy3HpgMz4vAAyZWzAxvyA1zW0no9GOLcPQnYaNUN/Fy2SYtETXTb0CQ9X1rt8ffkFP7ya+5TC83aMg==",
+ "x509cert": "MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo",
+ "x509certNew": "MIICVDCCAb2gAwIBAgIBADANBgkqhkiG9w0BAQ0FADBHMQswCQYDVQQGEwJ1czEQMA4GA1UECAwHZXhhbXBsZTEQMA4GA1UECgwHZXhhbXBsZTEUMBIGA1UEAwwLZXhhbXBsZS5jb20wHhcNMTcwNDA3MDgzMDAzWhcNMjcwNDA1MDgzMDAzWjBHMQswCQYDVQQGEwJ1czEQMA4GA1UECAwHZXhhbXBsZTEQMA4GA1UECgwHZXhhbXBsZTEUMBIGA1UEAwwLZXhhbXBsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAKhPS4/0azxbQekHHewQGKD7Pivr3CDpsrKxY3xlVanxj427OwzOb5KUVzsDEazumt6sZFY8HfidsjXY4EYA4ZzyL7ciIAR5vlAsIYN9nJ4AwVDnN/RjVwj+TN6BqWPLpVIpHc6Dl005HyE0zJnk1DZDn2tQVrIzbD3FhCp7YeotAgMBAAGjUDBOMB0GA1UdDgQWBBRYZx4thASfNvR/E7NsCF2IaZ7wIDAfBgNVHSMEGDAWgBRYZx4thASfNvR/E7NsCF2IaZ7wIDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBDQUAA4GBACz4aobx9aG3kh+rNyrlgM3K6dYfnKG1/YH5sJCAOvg8kDr0fQAQifH8lFVWumKUMoAe0bFTfwWtp/VJ8MprrEJth6PFeZdczpuv+fpLcNj2VmNVJqvQYvS4m36OnBFh1QFZW8UrbFIfdtm2nuZ+twSKqfKwjLdqcoX0p39h7Uw/"
+ },
+ "idp": {
+ "entityId": "http://idp.example.com/",
+ "singleSignOnService": {
+ "url": "http://idp.example.com/SSOService.php"
+ },
+ "singleLogoutService": {
+ "url": "http://idp.example.com/SingleLogoutService.php"
+ },
+ "x509cert": "MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo"
+ },
+ "security": {
+ "authnRequestsSigned": false,
+ "wantAssertionsSigned": false,
+ "signMetadata": false
+ },
+ "contactPerson": {
+ "technical": {
+ "givenName": "technical_name",
+ "emailAddress": "technical@example.com"
+ },
+ "support": {
+ "givenName": "support_name",
+ "emailAddress": "support@example.com"
+ }
+ },
+ "organization": {
+ "en-US": {
+ "name": "sp_test",
+ "displayname": "SP test",
+ "url": "http://sp.example.com"
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/src/OneLogin/saml2_tests/expensify_test.py.txt b/tests/src/OneLogin/saml2_tests/expensify_test.py.txt
deleted file mode 100644
index c02d164d..00000000
--- a/tests/src/OneLogin/saml2_tests/expensify_test.py.txt
+++ /dev/null
@@ -1,50 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright (c) 2014, OneLogin, Inc.
-# All rights reserved.
-
-from base64 import b64decode
-import json
-from lxml import etree
-from os.path import dirname, join, exists
-import unittest
-from teamcity import is_running_under_teamcity
-from teamcity.unittestpy import TeamcityTestRunner
-from xml.dom.minidom import Document, parseString
-
-from onelogin.saml2.constants import OneLogin_Saml2_Constants
-from onelogin.saml2.settings import OneLogin_Saml2_Settings
-from onelogin.saml2.utils import OneLogin_Saml2_Utils
-
-
-class OneLogin_Saml2_Utils_Test(unittest.TestCase):
- data_path = join(dirname(dirname(dirname(dirname(__file__)))), 'data')
-
- def file_contents(self, filename):
- f = open(filename, 'r')
- content = f.read()
- f.close()
- return content
-
-
- def testExpesify(self):
- xml = self.file_contents(join(self.data_path, 'responses', 'expensify.xml'))
- cert = """-----BEGIN CERTIFICATE-----
-MIIC5jCCAc6gAwIBAgIQejzcQ7LOo7NDphYfVqKB8jANBgkqhkiG9w0BAQsFADAv
-MS0wKwYDVQQDEyRBREZTIFNpZ25pbmcgLSBhZGZzLmZpbmlzaG1hc3Rlci5jb20w
-HhcNMTYwMzMwMjA1NjM2WhcNMTcwMzMwMjA1NjM2WjAvMS0wKwYDVQQDEyRBREZT
-IFNpZ25pbmcgLSBhZGZzLmZpbmlzaG1hc3Rlci5jb20wggEiMA0GCSqGSIb3DQEB
-AQUAA4IBDwAwggEKAoIBAQDrnGb3jm22UR0CqYQK55vj5dVMQIitpl5jUHuFgou0
-SsFfI5VJY5KNXQXCJoduXlxl12dRUQrd0h22TCLBWoneuDcor0UV4bRl7QyQWjuX
-M6pEDG8JsNWJEmnoiKGVmnfsaVgKTFZxO+Kydk/GRJauOqAyU7igABDpazMMHjjL
-c9iZ6tHjFjtWrUay3Hu3aaZheQWgzjnENUprgnH3zm5NQT5Dbd72z5TKrb4bbv08
-oInf52dnBNiSz0wa8uZRJPJJH2rGJmWUIJqp2fnhCxysMgf+ny5IfOkpcG4Cb/fk
-d4hoSmY5Bb/dKuD9/pTpmoylcinBtb3bMGASkulyQmENAgMBAAEwDQYJKoZIhvcN
-AQELBQADggEBAEx5l+gRoxUSY1nu6O2Cmu8WpnrFlEoNsko+Z/T34NYIm/2uaX6y
-kHoDSptgplwjtjE6lE0Zygm5R59BAU3tqGG1uHu4G7w++rcE6nnYunC3tZ8iKuzb
-VmkyUFjuf0AaWo/j/pl7zDJV1NHg6zm/NrqKhx4+Sr/LpVdBMOQmU9leqKsfVtkJ
-ktTOx1ChiEqu7R0yfKuaZwSlUfgnVtVS7yvLvk0KIbxhZe9BGdtJ03+XHYE0TeVN
-SpHh6Q1mylYurMX3WLMW5cQ+AcOWiDob2FuMq6wNWoUwOCvUwTu9NFQvuyzFpUrn
-wFgrDzL8lRc9nNA/iZh+pBIu+BM3/wWZLp8=
------END CERTIFICATE-----"""
- self.assertTrue(OneLogin_Saml2_Utils.validate_sign(xml, cert, debug=True))
diff --git a/tests/src/OneLogin/saml2_tests/settings_test.py b/tests/src/OneLogin/saml2_tests/settings_test.py
index 0069c0c8..b23f22f0 100644
--- a/tests/src/OneLogin/saml2_tests/settings_test.py
+++ b/tests/src/OneLogin/saml2_tests/settings_test.py
@@ -165,6 +165,21 @@ def testGetSPCert(self):
settings_3 = OneLogin_Saml2_Settings(settings_data, custom_base_path=custom_base_path)
self.assertIsNone(settings_3.get_sp_cert())
+ def testGetSPCertNew(self):
+ """
+ Tests the get_sp_cert_new method of the OneLogin_Saml2_Settings
+ """
+ settings_data = self.loadSettingsJSON()
+ cert = "-----BEGIN CERTIFICATE-----\nMIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMC\nTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYD\nVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG\n9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4\nMTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xi\nZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2Zl\naWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5v\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LO\nNoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHIS\nKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d\n1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8\nBUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7n\nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2Qar\nQ4/67OZfHd7R+POBXhophSMv1ZOo\n-----END CERTIFICATE-----\n"
+ settings = OneLogin_Saml2_Settings(settings_data)
+ self.assertEqual(cert, settings.get_sp_cert())
+ self.assertIsNone(settings.get_sp_cert_new())
+
+ settings = OneLogin_Saml2_Settings(self.loadSettingsJSON('settings7.json'))
+ cert_new = "-----BEGIN CERTIFICATE-----\nMIICVDCCAb2gAwIBAgIBADANBgkqhkiG9w0BAQ0FADBHMQswCQYDVQQGEwJ1czEQ\nMA4GA1UECAwHZXhhbXBsZTEQMA4GA1UECgwHZXhhbXBsZTEUMBIGA1UEAwwLZXhh\nbXBsZS5jb20wHhcNMTcwNDA3MDgzMDAzWhcNMjcwNDA1MDgzMDAzWjBHMQswCQYD\nVQQGEwJ1czEQMA4GA1UECAwHZXhhbXBsZTEQMA4GA1UECgwHZXhhbXBsZTEUMBIG\nA1UEAwwLZXhhbXBsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAKhP\nS4/0azxbQekHHewQGKD7Pivr3CDpsrKxY3xlVanxj427OwzOb5KUVzsDEazumt6s\nZFY8HfidsjXY4EYA4ZzyL7ciIAR5vlAsIYN9nJ4AwVDnN/RjVwj+TN6BqWPLpVIp\nHc6Dl005HyE0zJnk1DZDn2tQVrIzbD3FhCp7YeotAgMBAAGjUDBOMB0GA1UdDgQW\nBBRYZx4thASfNvR/E7NsCF2IaZ7wIDAfBgNVHSMEGDAWgBRYZx4thASfNvR/E7Ns\nCF2IaZ7wIDAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBDQUAA4GBACz4aobx9aG3\nkh+rNyrlgM3K6dYfnKG1/YH5sJCAOvg8kDr0fQAQifH8lFVWumKUMoAe0bFTfwWt\np/VJ8MprrEJth6PFeZdczpuv+fpLcNj2VmNVJqvQYvS4m36OnBFh1QFZW8UrbFIf\ndtm2nuZ+twSKqfKwjLdqcoX0p39h7Uw/\n-----END CERTIFICATE-----\n"
+ self.assertEqual(cert, settings.get_sp_cert())
+ self.assertEqual(cert_new, settings.get_sp_cert_new())
+
def testGetSPKey(self):
"""
Tests the get_sp_key method of the OneLogin_Saml2_Settings
@@ -337,6 +352,23 @@ def testGetSPMetadata(self):
self.assertIn(' ', metadata)
self.assertIn(' ', metadata)
self.assertIn('urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified ', metadata)
+ self.assertEquals(2, metadata.count('
Date: Fri, 12 May 2017 02:10:33 +0200
Subject: [PATCH 102/255] Be able to register more than 1 Identity Provider
x509cert, linked with an specific use (signing or encryption)
---
README.md | 40 ++++++++++++-
src/onelogin/saml2/logout_request.py | 29 ++++++++--
src/onelogin/saml2/logout_response.py | 22 +++++--
src/onelogin/saml2/response.py | 8 ++-
src/onelogin/saml2/settings.py | 26 ++++++++-
src/onelogin/saml2/utils.py | 17 +++++-
tests/settings/settings8.json | 58 +++++++++++++++++++
.../saml2_tests/logout_request_test.py | 47 +++++++++++++--
.../saml2_tests/logout_response_test.py | 26 +++++++--
.../src/OneLogin/saml2_tests/response_test.py | 18 ++++--
.../src/OneLogin/saml2_tests/settings_test.py | 14 ++++-
11 files changed, 274 insertions(+), 31 deletions(-)
create mode 100644 tests/settings/settings8.json
diff --git a/README.md b/README.md
index de0210f8..d96dc7d8 100644
--- a/README.md
+++ b/README.md
@@ -332,8 +332,24 @@ This is the settings.json file:
* Notice that if you want to validate any SAML Message sent by the HTTP-Redirect binding, you
* will need to provide the whole x509cert.
*/
- // 'certFingerprint' => '',
- // 'certFingerprintAlgorithm' => 'sha1',
+ // 'certFingerprint': '',
+ // 'certFingerprintAlgorithm': 'sha1',
+
+ /* 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 that parameter.
+ * (when used, 'x509cert' and 'certFingerprint' values are
+ * ignored).
+ */
+ // 'x509certMulti': {
+ // 'signing': [
+ // ''
+ // ],
+ // 'encryption': [
+ // ''
+ // ]
+ // }
}
}
```
@@ -827,6 +843,25 @@ else:
print ', '.join(errors)
```
+### SP Key rollover ###
+
+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.
+
+
+### IdP with multiple certificates ###
+
+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']['x509certMulti'] parameter.
+
+When that parameter is used, 'x509cert' and 'certFingerprint' values will be ignored by the toolkit.
+
+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
+
### Main classes and methods ###
@@ -944,6 +979,7 @@ Configuration of the OneLogin Python Toolkit
* ***get_contacts*** Gets contacts data.
* ***get_organization*** Gets organization data.
* ***format_idp_cert*** Formats the IdP cert.
+* ***format_idp_cert_multi*** Formats all registered IdP certs.
* ***format_sp_cert*** Formats the SP cert.
* ***format_sp_cert_new*** Formats the SP cert new.
* ***format_sp_key*** Formats the private key.
diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py
index 44dd4a52..99c97198 100644
--- a/src/onelogin/saml2/logout_request.py
+++ b/src/onelogin/saml2/logout_request.py
@@ -67,7 +67,13 @@ def __init__(self, settings, request=None, name_id=None, session_index=None, nq=
cert = None
if 'nameIdEncrypted' in security and security['nameIdEncrypted']:
- cert = idp_data['x509cert']
+ exists_multix509enc = 'x509certMulti' in idp_data and \
+ 'encryption' in idp_data['x509certMulti'] and \
+ idp_data['x509certMulti']['encryption']
+ if exists_multix509enc:
+ cert = idp_data['x509certMulti']['encryption'][0]
+ else:
+ cert = idp_data['x509cert']
if name_id is not None:
if name_id_format is not None:
@@ -380,19 +386,32 @@ def is_valid(self, request_data, raise_exceptions=False):
signed_query = '%s&RelayState=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'RelayState', lowercase_urlencoding=lowercase_urlencoding))
signed_query = '%s&SigAlg=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SigAlg', OneLogin_Saml2_Constants.RSA_SHA1, lowercase_urlencoding=lowercase_urlencoding))
- if 'x509cert' not in idp_data or not idp_data['x509cert']:
+ exists_x509cert = 'x509cert' in idp_data and idp_data['x509cert']
+ exists_multix509sign = 'x509certMulti' in idp_data and \
+ 'signing' in idp_data['x509certMulti'] and \
+ idp_data['x509certMulti']['signing']
+
+ if not (exists_x509cert or exists_multix509sign):
raise OneLogin_Saml2_Error(
'In order to validate the sign on the Logout Request, the x509cert of the IdP is required',
OneLogin_Saml2_Error.CERT_NOT_FOUND
)
- cert = idp_data['x509cert']
-
- if not OneLogin_Saml2_Utils.validate_binary_sign(signed_query, b64decode(get_data['Signature']), cert, sign_alg):
+ if exists_multix509sign:
+ for cert in idp_data['x509certMulti']['signing']:
+ if OneLogin_Saml2_Utils.validate_binary_sign(signed_query, b64decode(get_data['Signature']), cert, sign_alg):
+ return True
raise OneLogin_Saml2_ValidationError(
'Signature validation failed. Logout Request rejected',
OneLogin_Saml2_ValidationError.INVALID_SIGNATURE
)
+ else:
+ cert = idp_data['x509cert']
+ if not OneLogin_Saml2_Utils.validate_binary_sign(signed_query, b64decode(get_data['Signature']), cert, sign_alg):
+ raise OneLogin_Saml2_ValidationError(
+ 'Signature validation failed. Logout Request rejected',
+ OneLogin_Saml2_ValidationError.INVALID_SIGNATURE
+ )
return True
except Exception as err:
# pylint: disable=R0801sign_alg
diff --git a/src/onelogin/saml2/logout_response.py b/src/onelogin/saml2/logout_response.py
index 2653b210..f37a3585 100644
--- a/src/onelogin/saml2/logout_response.py
+++ b/src/onelogin/saml2/logout_response.py
@@ -146,18 +146,32 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False):
signed_query = '%s&RelayState=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'RelayState', lowercase_urlencoding=lowercase_urlencoding))
signed_query = '%s&SigAlg=%s' % (signed_query, OneLogin_Saml2_Utils.get_encoded_parameter(get_data, 'SigAlg', OneLogin_Saml2_Constants.RSA_SHA1, lowercase_urlencoding=lowercase_urlencoding))
- if 'x509cert' not in idp_data or not idp_data['x509cert']:
+ exists_x509cert = 'x509cert' in idp_data and idp_data['x509cert']
+ exists_multix509sign = 'x509certMulti' in idp_data and \
+ 'signing' in idp_data['x509certMulti'] and \
+ idp_data['x509certMulti']['signing']
+
+ if not (exists_x509cert or exists_multix509sign):
raise OneLogin_Saml2_Error(
'In order to validate the sign on the Logout Response, the x509cert of the IdP is required',
OneLogin_Saml2_Error.CERT_NOT_FOUND
)
- cert = idp_data['x509cert']
-
- if not OneLogin_Saml2_Utils.validate_binary_sign(signed_query, b64decode(get_data['Signature']), cert, sign_alg):
+ if exists_multix509sign:
+ for cert in idp_data['x509certMulti']['signing']:
+ if OneLogin_Saml2_Utils.validate_binary_sign(signed_query, b64decode(get_data['Signature']), cert, sign_alg):
+ return True
raise OneLogin_Saml2_ValidationError(
'Signature validation failed. Logout Response rejected',
OneLogin_Saml2_ValidationError.INVALID_SIGNATURE
)
+ else:
+ cert = idp_data['x509cert']
+
+ if not OneLogin_Saml2_Utils.validate_binary_sign(signed_query, b64decode(get_data['Signature']), cert, sign_alg):
+ raise OneLogin_Saml2_ValidationError(
+ 'Signature validation failed. Logout Response rejected',
+ OneLogin_Saml2_ValidationError.INVALID_SIGNATURE
+ )
return True
# pylint: disable=R0801
diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py
index ba524c2b..a97ac860 100644
--- a/src/onelogin/saml2/response.py
+++ b/src/onelogin/saml2/response.py
@@ -290,15 +290,19 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False):
fingerprint = idp_data.get('certFingerprint', None)
fingerprintalg = idp_data.get('certFingerprintAlgorithm', None)
+ multicerts = None
+ if 'x509certMulti' in idp_data and 'signing' in idp_data['x509certMulti'] and idp_data['x509certMulti']['signing']:
+ multicerts = idp_data['x509certMulti']['signing']
+
# If find a Signature on the Response, validates it checking the original response
- if has_signed_response and not OneLogin_Saml2_Utils.validate_sign(self.document, cert, fingerprint, fingerprintalg, xpath=OneLogin_Saml2_Utils.RESPONSE_SIGNATURE_XPATH, raise_exceptions=False):
+ if has_signed_response and not OneLogin_Saml2_Utils.validate_sign(self.document, cert, fingerprint, fingerprintalg, xpath=OneLogin_Saml2_Utils.RESPONSE_SIGNATURE_XPATH, multicerts=multicerts, raise_exceptions=False):
raise OneLogin_Saml2_ValidationError(
'Signature validation failed. SAML Response rejected',
OneLogin_Saml2_ValidationError.INVALID_SIGNATURE
)
document_check_assertion = self.decrypted_document if self.encrypted else self.document
- if has_signed_assertion and not OneLogin_Saml2_Utils.validate_sign(document_check_assertion, cert, fingerprint, fingerprintalg, xpath=OneLogin_Saml2_Utils.ASSERTION_SIGNATURE_XPATH, raise_exceptions=False):
+ if has_signed_assertion and not OneLogin_Saml2_Utils.validate_sign(document_check_assertion, cert, fingerprint, fingerprintalg, xpath=OneLogin_Saml2_Utils.ASSERTION_SIGNATURE_XPATH, multicerts=multicerts, raise_exceptions=False):
raise OneLogin_Saml2_ValidationError(
'Signature validation failed. SAML Response rejected',
OneLogin_Saml2_ValidationError.INVALID_SIGNATURE
diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py
index 8322eaf0..01bd5087 100644
--- a/src/onelogin/saml2/settings.py
+++ b/src/onelogin/saml2/settings.py
@@ -114,6 +114,8 @@ def __init__(self, settings=None, custom_base_path=None, sp_validation_only=Fals
if 'x509certNew' in self.__sp:
self.format_sp_cert_new()
self.format_sp_key()
+ if 'x509certMulti' in self.__idp:
+ self.format_idp_cert_multi()
def __load_paths(self, base_path=None):
"""
@@ -361,14 +363,21 @@ def check_idp_settings(self, settings):
exists_x509 = bool(idp.get('x509cert'))
exists_fingerprint = bool(idp.get('certFingerprint'))
+ exists_multix509sign = 'x509certMulti' in idp and \
+ 'signing' in idp['x509certMulti'] and \
+ idp['x509certMulti']['signing']
+ exists_multix509enc = 'x509certMulti' in idp and \
+ 'encryption' in idp['x509certMulti'] and \
+ idp['x509certMulti']['encryption']
+
want_assert_sign = bool(security.get('wantAssertionsSigned'))
want_mes_signed = bool(security.get('wantMessagesSigned'))
nameid_enc = bool(security.get('nameIdEncrypted'))
if (want_assert_sign or want_mes_signed) and \
- not(exists_x509 or exists_fingerprint):
+ not(exists_x509 or exists_fingerprint or exists_multix509sign):
errors.append('idp_cert_or_fingerprint_not_found_and_required')
- if nameid_enc and not exists_x509:
+ if nameid_enc and not (exists_x509 or exists_multix509enc):
errors.append('idp_cert_not_found_and_required')
return errors
@@ -722,6 +731,19 @@ def format_idp_cert(self):
"""
self.__idp['x509cert'] = OneLogin_Saml2_Utils.format_cert(self.__idp['x509cert'])
+ def format_idp_cert_multi(self):
+ """
+ Formats the Multple IdP certs.
+ """
+ if 'x509certMulti' in self.__idp:
+ if 'signing' in self.__idp['x509certMulti']:
+ for idx in range(len(self.__idp['x509certMulti']['signing'])):
+ self.__idp['x509certMulti']['signing'][idx] = OneLogin_Saml2_Utils.format_cert(self.__idp['x509certMulti']['signing'][idx])
+
+ if 'encryption' in self.__idp['x509certMulti']:
+ for idx in range(len(self.__idp['x509certMulti']['encryption'])):
+ self.__idp['x509certMulti']['encryption'][idx] = OneLogin_Saml2_Utils.format_cert(self.__idp['x509certMulti']['encryption'][idx])
+
def format_sp_cert(self):
"""
Formats the SP cert.
diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py
index 00fefae3..826c8a87 100644
--- a/src/onelogin/saml2/utils.py
+++ b/src/onelogin/saml2/utils.py
@@ -915,7 +915,7 @@ def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constant
@staticmethod
@return_false_on_exception
- def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', validatecert=False, debug=False, xpath=None):
+ def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', validatecert=False, debug=False, xpath=None, multicerts=None):
"""
Validates a signature (Message or Assertion).
@@ -940,6 +940,9 @@ def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', valid
:param xpath: The xpath of the signed element
:type: string
+ :param multicerts: Multiple public certs
+ :type: list
+
:param raise_exceptions: Whether to return false on failure or raise an exception
:type raise_exceptions: Boolean
"""
@@ -986,7 +989,17 @@ def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', valid
if len(signature_nodes) == 1:
signature_node = signature_nodes[0]
- return OneLogin_Saml2_Utils.validate_node_sign(signature_node, elem, cert, fingerprint, fingerprintalg, validatecert, debug, raise_exceptions=True)
+ if not multicerts:
+ return OneLogin_Saml2_Utils.validate_node_sign(signature_node, elem, cert, fingerprint, fingerprintalg, validatecert, debug, raise_exceptions=True)
+ else:
+ # If multiple certs are provided, I may ignore cert and
+ # fingerprint provided by the method and just check the
+ # certs multicerts
+ fingerprint = fingerprintalg = None
+ for cert in multicerts:
+ if OneLogin_Saml2_Utils.validate_node_sign(signature_node, elem, cert, fingerprint, fingerprintalg, validatecert, False, raise_exceptions=False):
+ return True
+ raise OneLogin_Saml2_ValidationError('Signature validation failed. SAML Response rejected.')
else:
raise OneLogin_Saml2_ValidationError('Expected exactly one signature node; got {}.'.format(len(signature_nodes)), OneLogin_Saml2_ValidationError.WRONG_NUMBER_OF_SIGNATURES)
diff --git a/tests/settings/settings8.json b/tests/settings/settings8.json
new file mode 100644
index 00000000..ce30e498
--- /dev/null
+++ b/tests/settings/settings8.json
@@ -0,0 +1,58 @@
+{
+ "strict": false,
+ "debug": false,
+ "custom_base_path": "../../../tests/data/customPath/",
+ "sp": {
+ "entityId": "http://stuff.com/endpoints/metadata.php",
+ "assertionConsumerService": {
+ "url": "http://stuff.com/endpoints/endpoints/acs.php"
+ },
+ "singleLogoutService": {
+ "url": "http://stuff.com/endpoints/endpoints/sls.php"
+ },
+ "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified",
+ "privateKey": "MIICXgIBAAKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABAoGAD4/Z4LWVWV6D1qMIp1Gzr0ZmdWTE1SPdZ7Ej8glGnCzPdguCPuzbhGXmIg0VJ5D+02wsqws1zd48JSMXXM8zkYZVwQYIPUsNn5FetQpwxDIMPmhHg+QNBgwOnk8JK2sIjjLPL7qY7Itv7LT7Gvm5qSOkZ33RCgXcgz+okEIQMYkCQQDzbTOyDL0c5WQV6A2k06T/azdhUdGXF9C0+WkWSfNaovmTgRXh1G+jMlr82Snz4p4/STt7P/XtyWzF3pkVgZr3AkEA7nPjXwHlttNEMo6AtxHd47nizK2NUN803ElIUT8P9KSCoERmSXq66PDekGNic4ldpsSvOeYCk8MAYoDBy9kvVwJBAMLgX4xg6lzhv7hR5+pWjTb1rIY6rCHbrPfU264+UZXz9v2BT/VUznLF81WMvStD9xAPHpFS6R0OLghSZhdzhI0CQQDL8Duvfxzrn4b9QlmduV8wLERoT6rEVxKLsPVz316TGrxJvBZLk/cV0SRZE1cZf4ukXSWMfEcJ/0Zt+LdG1CqjAkEAqwLSglJ9Dy3HpgMz4vAAyZWzAxvyA1zW0no9GOLcPQnYaNUN/Fy2SYtETXTb0CQ9X1rt8ffkFP7ya+5TC83aMg==",
+ "x509cert": "MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo"
+ },
+ "idp": {
+ "entityId": "http://idp.example.com/",
+ "singleSignOnService": {
+ "url": "http://idp.example.com/SSOService.php"
+ },
+ "singleLogoutService": {
+ "url": "http://idp.example.com/SingleLogoutService.php"
+ },
+ "x509cert": "",
+ "x509certMulti": {
+ "signing": [
+ "MIICbDCCAdWgAwIBAgIBADANBgkqhkiG9w0BAQ0FADBTMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRgwFgYDVQQDDA9pZHAuZXhhbXBsZS5jb20wHhcNMTQwOTIzMTIyNDA4WhcNNDIwMjA4MTIyNDA4WjBTMQswCQYDVQQGEwJ1czETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UECgwMT25lbG9naW4gSW5jMRgwFgYDVQQDDA9pZHAuZXhhbXBsZS5jb20wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAOWA+YHU7cvPOrBOfxCscsYTJB+kH3MaA9BFrSHFS+KcR6cw7oPSktIJxUgvDpQbtfNcOkE/tuOPBDoech7AXfvH6d7Bw7xtW8PPJ2mB5Hn/HGW2roYhxmfh3tR5SdwN6i4ERVF8eLkvwCHsNQyK2Ref0DAJvpBNZMHCpS24916/AgMBAAGjUDBOMB0GA1UdDgQWBBQ77/qVeiigfhYDITplCNtJKZTM8DAfBgNVHSMEGDAWgBQ77/qVeiigfhYDITplCNtJKZTM8DAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBDQUAA4GBAJO2j/1uO80E5C2PM6Fk9mzerrbkxl7AZ/mvlbOn+sNZE+VZ1AntYuG8ekbJpJtG1YfRfc7EA9mEtqvv4dhv7zBy4nK49OR+KpIBjItWB5kYvrqMLKBa32sMbgqqUqeF1ENXKjpvLSuPdfGJZA3dNa/+Dyb8GGqWe707zLyc5F8m",
+ "MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo"
+ ],
+ "encryption": [
+ "MIICgTCCAeoCCQCbOlrWDdX7FTANBgkqhkiG9w0BAQUFADCBhDELMAkGA1UEBhMCTk8xGDAWBgNVBAgTD0FuZHJlYXMgU29sYmVyZzEMMAoGA1UEBxMDRm9vMRAwDgYDVQQKEwdVTklORVRUMRgwFgYDVQQDEw9mZWlkZS5lcmxhbmcubm8xITAfBgkqhkiG9w0BCQEWEmFuZHJlYXNAdW5pbmV0dC5ubzAeFw0wNzA2MTUxMjAxMzVaFw0wNzA4MTQxMjAxMzVaMIGEMQswCQYDVQQGEwJOTzEYMBYGA1UECBMPQW5kcmVhcyBTb2xiZXJnMQwwCgYDVQQHEwNGb28xEDAOBgNVBAoTB1VOSU5FVFQxGDAWBgNVBAMTD2ZlaWRlLmVybGFuZy5ubzEhMB8GCSqGSIb3DQEJARYSYW5kcmVhc0B1bmluZXR0Lm5vMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDivbhR7P516x/S3BqKxupQe0LONoliupiBOesCO3SHbDrl3+q9IbfnfmE04rNuMcPsIxB161TdDpIesLCn7c8aPHISKOtPlAeTZSnb8QAu7aRjZq3+PbrP5uW3TcfCGPtKTytHOge/OlJbo078dVhXQ14d1EDwXJW1rRXuUt4C8QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACDVfp86HObqY+e8BUoWQ9+VMQx1ASDohBjwOsg2WykUqRXF+dLfcUH9dWR63CtZIKFDbStNomPnQz7nbK+onygwBspVEbnHuUihZq3ZUdmumQqCw4Uvs/1Uvq3orOo/WJVhTyvLgFVK2QarQ4/67OZfHd7R+POBXhophSMv1ZOo"
+ ]
+ }
+ },
+ "security": {
+ "authnRequestsSigned": false,
+ "wantAssertionsSigned": false,
+ "signMetadata": false
+ },
+ "contactPerson": {
+ "technical": {
+ "givenName": "technical_name",
+ "emailAddress": "technical@example.com"
+ },
+ "support": {
+ "givenName": "support_name",
+ "emailAddress": "support@example.com"
+ }
+ },
+ "organization": {
+ "en-US": {
+ "name": "sp_test",
+ "displayname": "SP test",
+ "url": "http://sp.example.com"
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/src/OneLogin/saml2_tests/logout_request_test.py b/tests/src/OneLogin/saml2_tests/logout_request_test.py
index 5b5cd56b..b0bfdf92 100644
--- a/tests/src/OneLogin/saml2_tests/logout_request_test.py
+++ b/tests/src/OneLogin/saml2_tests/logout_request_test.py
@@ -22,15 +22,13 @@ class OneLogin_Saml2_Logout_Request_Test(unittest.TestCase):
data_path = join(dirname(dirname(dirname(dirname(__file__)))), 'data')
settings_path = join(dirname(dirname(dirname(dirname(__file__)))), 'settings')
- def loadSettingsJSON(self):
- filename = join(self.settings_path, 'settings1.json')
+ def loadSettingsJSON(self, name='settings1.json'):
+ filename = join(self.settings_path, name)
if exists(filename):
stream = open(filename, 'r')
settings = json.load(stream)
stream.close()
return settings
- else:
- raise Exception('Settings json file does not exist')
def file_contents(self, filename):
f = open(filename, 'r')
@@ -57,6 +55,27 @@ def testConstructor(self):
inflated = OneLogin_Saml2_Utils.decode_base64_and_inflate(payload)
self.assertRegexpMatches(inflated, '^')
+
def testCreateDeflatedSAMLLogoutRequestURLParameter(self):
"""
Tests the OneLogin_Saml2_LogoutRequest Constructor.
@@ -426,6 +445,26 @@ def testIsValidSign(self):
self.assertFalse(logout_request7.is_valid(request_data))
self.assertEqual('In order to validate the sign on the Logout Request, the x509cert of the IdP is required', logout_request7.get_error())
+ def testIsValidSignUsingX509certMulti(self):
+ """
+ Tests the is_valid method of the OneLogin_Saml2_LogoutRequest
+ """
+ request_data = {
+ 'http_host': 'example.com',
+ 'script_name': 'index.html',
+ 'get_data': {
+ 'SAMLRequest': 'fZJNa+MwEIb/itHdiTz6sC0SQyEsBPoB27KHXoIsj7cGW3IlGfLzV7G7kN1DL2KYmeedmRcdgp7GWT26326JP/FzwRCz6zTaoNbKkSzeKqfDEJTVEwYVjXp9eHpUsKNq9i4640Zyh3xP6BDQx8FZkp1PR3KpqexAl72QmpUCS8SW01IiZz2TVVGD4X1VQYlAsl/oQyKPJAklPIQFzzZEbWNK0YLnlOVA3wqpQCoB7yQ7pWsGq+NKfcQ4q/0+xKXvd8ZNe7Td7AYbw10UxrCbP2aSPbv4Yl/8Qx/R3+SB5bTOoXiDQvFNvjnc7lXrIr75kh+6eYdXPc0jrkMO+/umjXhOtpxP2Q/nJx2/9+uWGbq8X1tV9NqGAW0kzaVvoe1AAJeCSWqYaUVRM2SilKKuqDTpFSlszdcK29RthVm9YriZebYdXpsLdhVAB7VJzif3haYMqqTVcl0JMBR4y+s2zak3sf/4v8l/vlHzBw==',
+ 'RelayState': '_1037fbc88ec82ce8e770b2bed1119747bb812a07e6',
+ 'SigAlg': 'http://www.w3.org/2000/09/xmldsig#rsa-sha1',
+ 'Signature': 'Ouxo9BV6zmq4yrgamT9EbSKy/UmvSxGS8z26lIMgKOEP4LFR/N23RftdANmo4HafrzSfA0YTXwhKDqbOByS0j+Ql8OdQOes7vGioSjo5qq/Bi+5i6jXwQfphnfcHAQiJL4gYVIifkhhHRWpvYeiysF1Y9J02me0izwazFmoRXr4='
+ }
+ }
+ settings_info = self.loadSettingsJSON('settings8.json')
+ settings_info['strict'] = False
+ settings = OneLogin_Saml2_Settings(settings_info)
+ logout_request = OneLogin_Saml2_Logout_Request(settings, request_data['get_data']['SAMLRequest'])
+ self.assertTrue(logout_request.is_valid(request_data))
+
def testGetXML(self):
"""
Tests that we can get the logout request XML directly without
diff --git a/tests/src/OneLogin/saml2_tests/logout_response_test.py b/tests/src/OneLogin/saml2_tests/logout_response_test.py
index 3f86df19..f54690ed 100644
--- a/tests/src/OneLogin/saml2_tests/logout_response_test.py
+++ b/tests/src/OneLogin/saml2_tests/logout_response_test.py
@@ -22,15 +22,13 @@ class OneLogin_Saml2_Logout_Response_Test(unittest.TestCase):
data_path = join(dirname(dirname(dirname(dirname(__file__)))), 'data')
settings_path = join(dirname(dirname(dirname(dirname(__file__)))), 'settings')
- def loadSettingsJSON(self):
- filename = join(self.settings_path, 'settings1.json')
+ def loadSettingsJSON(self, name='settings1.json'):
+ filename = join(self.settings_path, name)
if exists(filename):
stream = open(filename, 'r')
settings = json.load(stream)
stream.close()
return settings
- else:
- raise Exception('Settings json file does not exist')
def file_contents(self, filename):
f = open(filename, 'r')
@@ -367,6 +365,26 @@ def testIsValid(self):
response_3 = OneLogin_Saml2_Logout_Response(settings, message_3)
self.assertTrue(response_3.is_valid(request_data))
+ def testIsValidSignUsingX509certMulti(self):
+ """
+ Tests the is_valid method of the OneLogin_Saml2_LogoutResponse
+ """
+ request_data = {
+ 'http_host': 'example.com',
+ 'script_name': 'index.html',
+ 'get_data': {
+ 'SAMLResponse': 'fZHbasJAEIZfJey9ZrNZc1gSodRSBKtQxYveyGQz1kCyu2Q24OM3jS21UHo3p++f4Z+CoGud2th3O/hXJGcNYXDtWkNqapVs6I2yQA0pAx2S8lrtH142Ssy5cr31VtuW3SH/E0CEvW+sYcF6VbLTIktFLMWZgxQR8DSP85wDB4GJGMOqShYVaoBUsOCIPY1kyUahEScacG3Ig/FjiUdyxuOZ4IcoUVGq4vSNBSsk3xjwE3Xx3qkwJD+cz3NtuxBN7WxjPN1F1NLcXdwob77tONiS7bZPm93zenvCqopxgVJmuU50jREsZF4noKWAOuNZJbNznnBky+LTDDVd2S+/dje1m+MVOtfidEER3g8Vt2fsPfiBfmePtsbgCO2A/9tL07TaD1ojEQuXtw0/ouFfD19+AA==',
+ 'RelayState': 'http://stuff.com/endpoints/endpoints/index.php',
+ 'SigAlg': 'http://www.w3.org/2000/09/xmldsig#rsa-sha1',
+ 'Signature': 'OV9c4R0COSjN69fAKCpV7Uj/yx6/KFxvbluVCzdK3UuortpNMpgHFF2wYNlMSG9GcYGk6p3I8nB7Z+1TQchMWZOlO/StjAqgtZhtpiwPcWryNuq8vm/6hnJ3zMDhHTS7F8KG4qkCXmJ9sQD3Y31UNcuygBwIbNakvhDT5Qo9Nsw='
+ }
+ }
+ settings_info = self.loadSettingsJSON('settings8.json')
+ settings_info['strict'] = False
+ settings = OneLogin_Saml2_Settings(settings_info)
+ logout_response = OneLogin_Saml2_Logout_Response(settings, request_data['get_data']['SAMLResponse'])
+ self.assertTrue(logout_response.is_valid(request_data))
+
def testGetXML(self):
"""
Tests that we can get the logout response XML directly without
diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py
index d7ac4673..5d6bebf2 100644
--- a/tests/src/OneLogin/saml2_tests/response_test.py
+++ b/tests/src/OneLogin/saml2_tests/response_test.py
@@ -22,12 +22,10 @@
class OneLogin_Saml2_Response_Test(unittest.TestCase):
data_path = join(dirname(dirname(dirname(dirname(__file__)))), 'data')
+ settings_path = join(dirname(dirname(dirname(dirname(__file__)))), 'settings')
- def loadSettingsJSON(self, filename=None):
- if filename:
- filename = join(dirname(dirname(dirname(dirname(__file__)))), 'settings', filename)
- else:
- filename = join(dirname(dirname(dirname(dirname(__file__)))), 'settings', 'settings1.json')
+ def loadSettingsJSON(self, name='settings1.json'):
+ filename = join(self.settings_path, name)
if exists(filename):
stream = open(filename, 'r')
settings = json.load(stream)
@@ -1354,6 +1352,16 @@ def testIsValidSign(self):
# Modified message
self.assertFalse(response_9.is_valid(self.get_request_data()))
+ def testIsValidSignUsingX509certMulti(self):
+ """
+ Tests the is_valid method of the OneLogin_Saml2_Response
+ Case Using x509certMulti
+ """
+ settings = OneLogin_Saml2_Settings(self.loadSettingsJSON('settings8.json'))
+ xml = self.file_contents(join(self.data_path, 'responses', 'signed_message_response.xml.base64'))
+ response = OneLogin_Saml2_Response(settings, xml)
+ self.assertTrue(response.is_valid(self.get_request_data()))
+
def testIsValidSignWithEmptyReferenceURI(self):
settings_info = self.loadSettingsJSON()
del settings_info['idp']['x509cert']
diff --git a/tests/src/OneLogin/saml2_tests/settings_test.py b/tests/src/OneLogin/saml2_tests/settings_test.py
index b23f22f0..df08a6a7 100644
--- a/tests/src/OneLogin/saml2_tests/settings_test.py
+++ b/tests/src/OneLogin/saml2_tests/settings_test.py
@@ -356,7 +356,7 @@ def testGetSPMetadata(self):
self.assertEquals(1, metadata.count('
Date: Fri, 12 May 2017 16:53:41 +0200
Subject: [PATCH 103/255] Allow metadata to be retrieved from source containing
data for multiple entities
---
README.md | 20 +++++++
src/onelogin/saml2/idp_metadata_parser.py | 22 ++++++--
.../metadata/idp_multiple_descriptors.xml | 53 +++++++++++++++++++
.../saml2_tests/idp_metadata_parser_test.py | 38 +++++++++++++
4 files changed, 129 insertions(+), 4 deletions(-)
create mode 100644 tests/data/metadata/idp_multiple_descriptors.xml
diff --git a/README.md b/README.md
index d96dc7d8..7ad159f3 100644
--- a/README.md
+++ b/README.md
@@ -503,6 +503,23 @@ json_data_file.close()
auth = OneLogin_Saml2_Auth(req, settings_data)
```
+#### Metadata Based Configuration
+
+The method above requires a little extra work to manually specify attributes about the IdP. (And your SP application)
+
+There's an easier method -- use a metadata exchange. Metadata is just an XML file that defines the capabilities of both the IdP and the SP application. It also contains the X.509 public key certificates which add to the trusted relationship. The IdP administrator can also configure custom settings for an SP based on the metadata.
+
+Using ````parse_remote```` IdP metadata can be obtained and added to the settings withouth further ado.
+
+``
+idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote('https://example.com/auth/saml2/idp/metadata')
+``
+
+If the Metadata contains several entities, the relevant EntityDescriptor can be specified when retrieving the settings from the IdpMetadataParser by its Entity Id value:
+
+idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote(https://example.com/metadatas, entity_id='idp_entity_id')
+
+
#### How load the library ####
In order to use the toolkit library you need to import the file that contains the class that you will need
@@ -1042,6 +1059,9 @@ A class that contains methods to obtain and parse metadata from IdP
For more info, look at the source code; each method is documented and details about what does and how to use it are provided. Make sure to also check the doc folder where HTML documentation about the classes and methods is provided.
+
+
+
Demos included in the toolkit
-----------------------------
diff --git a/src/onelogin/saml2/idp_metadata_parser.py b/src/onelogin/saml2/idp_metadata_parser.py
index fc9689cb..28154820 100644
--- a/src/onelogin/saml2/idp_metadata_parser.py
+++ b/src/onelogin/saml2/idp_metadata_parser.py
@@ -60,24 +60,29 @@ def get_metadata(url, validate_cert=True):
return xml
@staticmethod
- def parse_remote(url, validate_cert=True, **kwargs):
+ def parse_remote(url, validate_cert=True, entity_id=None, **kwargs):
"""
Gets the metadata XML from the provided URL and parse it, returning a dict with extracted data
:param url: Url where the XML of the Identity Provider Metadata is published.
:type url: string
+ :param entity_id: Specify the entity_id of the EntityDescriptor that you want to parse a XML
+ that contains multiple EntityDescriptor.
+ :type entity_id: string
+
:returns: settings dict with extracted data
:rtype: dict
"""
idp_metadata = OneLogin_Saml2_IdPMetadataParser.get_metadata(url, validate_cert)
- return OneLogin_Saml2_IdPMetadataParser.parse(idp_metadata, **kwargs)
+ return OneLogin_Saml2_IdPMetadataParser.parse(idp_metadata, entity_id=entity_id, **kwargs)
@staticmethod
def parse(
idp_metadata,
required_sso_binding=OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT,
- required_slo_binding=OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT):
+ required_slo_binding=OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT,
+ entity_id=None):
"""
Parse the Identity Provider metadata and return a dict with extracted data.
@@ -104,13 +109,22 @@ def parse(
:type required_slo_binding: one of OneLogin_Saml2_Constants.BINDING_HTTP_REDIRECT
or OneLogin_Saml2_Constants.BINDING_HTTP_POST
+ :param entity_id: Specify the entity_id of the EntityDescriptor that you want to parse a XML
+ that contains multiple EntityDescriptor.
+ :type entity_id: string
+
:returns: settings dict with extracted data
:rtype: dict
"""
data = {}
dom = fromstring(idp_metadata)
- entity_descriptor_nodes = OneLogin_Saml2_Utils.query(dom, '//md:EntityDescriptor')
+
+ entity_desc_path = '//md:EntityDescriptor'
+ if entity_id:
+ entity_desc_path += "[@entityID='%s']" % entity_id
+
+ entity_descriptor_nodes = OneLogin_Saml2_Utils.query(dom, entity_desc_path)
idp_entity_id = want_authn_requests_signed = idp_name_id_format = idp_sso_url = idp_slo_url = idp_x509_cert = None
diff --git a/tests/data/metadata/idp_multiple_descriptors.xml b/tests/data/metadata/idp_multiple_descriptors.xml
new file mode 100644
index 00000000..c77face7
--- /dev/null
+++ b/tests/data/metadata/idp_multiple_descriptors.xml
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+ LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURxekNDQXhTZ0F3SUJBZ0lCQVRBTkJna3Foa2lHOXcwQkFRc0ZBRENCaGpFTE1Ba0dBMVVFQmhNQ1FWVXgKRERBS0JnTlZCQWdUQTA1VFZ6RVBNQTBHQTFVRUJ4TUdVM2xrYm1WNU1Rd3dDZ1lEVlFRS0RBTlFTVlF4Q1RBSApCZ05WQkFzTUFERVlNQllHQTFVRUF3d1BiR0YzY21WdVkyVndhWFF1WTI5dE1TVXdJd1lKS29aSWh2Y05BUWtCCkRCWnNZWGR5Wlc1alpTNXdhWFJBWjIxaGFXd3VZMjl0TUI0WERURXlNRFF4T1RJeU5UUXhPRm9YRFRNeU1EUXgKTkRJeU5UUXhPRm93Z1lZeEN6QUpCZ05WQkFZVEFrRlZNUXd3Q2dZRFZRUUlFd05PVTFjeER6QU5CZ05WQkFjVApCbE41Wkc1bGVURU1NQW9HQTFVRUNnd0RVRWxVTVFrd0J3WURWUVFMREFBeEdEQVdCZ05WQkFNTUQyeGhkM0psCmJtTmxjR2wwTG1OdmJURWxNQ01HQ1NxR1NJYjNEUUVKQVF3V2JHRjNjbVZ1WTJVdWNHbDBRR2R0WVdsc0xtTnYKYlRDQm56QU5CZ2txaGtpRzl3MEJBUUVGQUFPQmpRQXdnWWtDZ1lFQXFqaWUzUjJvaStwRGFldndJeXMvbWJVVApubkdsa3h0ZGlrcnExMXZleHd4SmlQTmhtaHFSVzNtVXVKRXpsbElkVkw2RW14R1lUcXBxZjkzSGxoa3NhZUowCjhVZ2pQOVVtTVlyaFZKdTFqY0ZXVjdmei9yKzIxL2F3VG5EVjlzTVlRcXVJUllZeTdiRzByMU9iaXdkb3ZudGsKN2dGSTA2WjB2WmFjREU1Ym9xVUNBd0VBQWFPQ0FTVXdnZ0VoTUFrR0ExVWRFd1FDTUFBd0N3WURWUjBQQkFRRApBZ1VnTUIwR0ExVWREZ1FXQkJTUk9OOEdKOG8rOGpnRnRqa3R3WmRxeDZCUnlUQVRCZ05WSFNVRUREQUtCZ2dyCkJnRUZCUWNEQVRBZEJnbGdoa2dCaHZoQ0FRMEVFQllPVkdWemRDQllOVEE1SUdObGNuUXdnYk1HQTFVZEl3U0IKcXpDQnFJQVVrVGpmQmlmS1B2STRCYlk1TGNHWGFzZWdVY21oZ1l5a2dZa3dnWVl4Q3pBSkJnTlZCQVlUQWtGVgpNUXd3Q2dZRFZRUUlFd05PVTFjeER6QU5CZ05WQkFjVEJsTjVaRzVsZVRFTU1Bb0dBMVVFQ2d3RFVFbFVNUWt3CkJ3WURWUVFMREFBeEdEQVdCZ05WQkFNTUQyeGhkM0psYm1ObGNHbDBMbU52YlRFbE1DTUdDU3FHU0liM0RRRUoKQVF3V2JHRjNjbVZ1WTJVdWNHbDBRR2R0WVdsc0xtTnZiWUlCQVRBTkJna3Foa2lHOXcwQkFRc0ZBQU9CZ1FDRQpUQWVKVERTQVc2ejFVRlRWN1FyZWg0VUxGT1JhajkrZUN1RjNLV0RIYyswSVFDajlyZG5ERzRRL3dmNy9yYVEwCkpuUFFDU0NkclBMSmV5b1BIN1FhVHdvYUY3ZHpWdzRMQ3N5TkpURld4NGNNNTBWdzZSNWZET2dpQzhic2ZmUzgKQkptb3VscnJaRE5OVmpHOG1XNmNMeHJZdlZRT3JSVmVjQ0ZJZ3NzQ2JBPT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=
+
+
+
+
+
+
+ LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURxekNDQXhTZ0F3SUJBZ0lCQVRBTkJna3Foa2lHOXcwQkFRc0ZBRENCaGpFTE1Ba0dBMVVFQmhNQ1FWVXgKRERBS0JnTlZCQWdUQTA1VFZ6RVBNQTBHQTFVRUJ4TUdVM2xrYm1WNU1Rd3dDZ1lEVlFRS0RBTlFTVlF4Q1RBSApCZ05WQkFzTUFERVlNQllHQTFVRUF3d1BiR0YzY21WdVkyVndhWFF1WTI5dE1TVXdJd1lKS29aSWh2Y05BUWtCCkRCWnNZWGR5Wlc1alpTNXdhWFJBWjIxaGFXd3VZMjl0TUI0WERURXlNRFF4T1RJeU5UUXhPRm9YRFRNeU1EUXgKTkRJeU5UUXhPRm93Z1lZeEN6QUpCZ05WQkFZVEFrRlZNUXd3Q2dZRFZRUUlFd05PVTFjeER6QU5CZ05WQkFjVApCbE41Wkc1bGVURU1NQW9HQTFVRUNnd0RVRWxVTVFrd0J3WURWUVFMREFBeEdEQVdCZ05WQkFNTUQyeGhkM0psCmJtTmxjR2wwTG1OdmJURWxNQ01HQ1NxR1NJYjNEUUVKQVF3V2JHRjNjbVZ1WTJVdWNHbDBRR2R0WVdsc0xtTnYKYlRDQm56QU5CZ2txaGtpRzl3MEJBUUVGQUFPQmpRQXdnWWtDZ1lFQXFqaWUzUjJvaStwRGFldndJeXMvbWJVVApubkdsa3h0ZGlrcnExMXZleHd4SmlQTmhtaHFSVzNtVXVKRXpsbElkVkw2RW14R1lUcXBxZjkzSGxoa3NhZUowCjhVZ2pQOVVtTVlyaFZKdTFqY0ZXVjdmei9yKzIxL2F3VG5EVjlzTVlRcXVJUllZeTdiRzByMU9iaXdkb3ZudGsKN2dGSTA2WjB2WmFjREU1Ym9xVUNBd0VBQWFPQ0FTVXdnZ0VoTUFrR0ExVWRFd1FDTUFBd0N3WURWUjBQQkFRRApBZ1VnTUIwR0ExVWREZ1FXQkJTUk9OOEdKOG8rOGpnRnRqa3R3WmRxeDZCUnlUQVRCZ05WSFNVRUREQUtCZ2dyCkJnRUZCUWNEQVRBZEJnbGdoa2dCaHZoQ0FRMEVFQllPVkdWemRDQllOVEE1SUdObGNuUXdnYk1HQTFVZEl3U0IKcXpDQnFJQVVrVGpmQmlmS1B2STRCYlk1TGNHWGFzZWdVY21oZ1l5a2dZa3dnWVl4Q3pBSkJnTlZCQVlUQWtGVgpNUXd3Q2dZRFZRUUlFd05PVTFjeER6QU5CZ05WQkFjVEJsTjVaRzVsZVRFTU1Bb0dBMVVFQ2d3RFVFbFVNUWt3CkJ3WURWUVFMREFBeEdEQVdCZ05WQkFNTUQyeGhkM0psYm1ObGNHbDBMbU52YlRFbE1DTUdDU3FHU0liM0RRRUoKQVF3V2JHRjNjbVZ1WTJVdWNHbDBRR2R0WVdsc0xtTnZiWUlCQVRBTkJna3Foa2lHOXcwQkFRc0ZBQU9CZ1FDRQpUQWVKVERTQVc2ejFVRlRWN1FyZWg0VUxGT1JhajkrZUN1RjNLV0RIYyswSVFDajlyZG5ERzRRL3dmNy9yYVEwCkpuUFFDU0NkclBMSmV5b1BIN1FhVHdvYUY3ZHpWdzRMQ3N5TkpURld4NGNNNTBWdzZSNWZET2dpQzhic2ZmUzgKQkptb3VscnJaRE5OVmpHOG1XNmNMeHJZdlZRT3JSVmVjQ0ZJZ3NzQ2JBPT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=
+
+
+
+
+ urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified
+ urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress
+ urn:oasis:names:tc:SAML:2.0:nameid-format:persistent
+
+
+
+
+
+
+
+
+
+
+ LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURxekNDQXhTZ0F3SUJBZ0lCQVRBTkJna3Foa2lHOXcwQkFRc0ZBRENCaGpFTE1Ba0dBMVVFQmhNQ1FWVXgKRERBS0JnTlZCQWdUQTA1VFZ6RVBNQTBHQTFVRUJ4TUdVM2xrYm1WNU1Rd3dDZ1lEVlFRS0RBTlFTVlF4Q1RBSApCZ05WQkFzTUFERVlNQllHQTFVRUF3d1BiR0YzY21WdVkyVndhWFF1WTI5dE1TVXdJd1lKS29aSWh2Y05BUWtCCkRCWnNZWGR5Wlc1alpTNXdhWFJBWjIxaGFXd3VZMjl0TUI0WERURXlNRFF4T1RJeU5UUXhPRm9YRFRNeU1EUXgKTkRJeU5UUXhPRm93Z1lZeEN6QUpCZ05WQkFZVEFrRlZNUXd3Q2dZRFZRUUlFd05PVTFjeER6QU5CZ05WQkFjVApCbE41Wkc1bGVURU1NQW9HQTFVRUNnd0RVRWxVTVFrd0J3WURWUVFMREFBeEdEQVdCZ05WQkFNTUQyeGhkM0psCmJtTmxjR2wwTG1OdmJURWxNQ01HQ1NxR1NJYjNEUUVKQVF3V2JHRjNjbVZ1WTJVdWNHbDBRR2R0WVdsc0xtTnYKYlRDQm56QU5CZ2txaGtpRzl3MEJBUUVGQUFPQmpRQXdnWWtDZ1lFQXFqaWUzUjJvaStwRGFldndJeXMvbWJVVApubkdsa3h0ZGlrcnExMXZleHd4SmlQTmhtaHFSVzNtVXVKRXpsbElkVkw2RW14R1lUcXBxZjkzSGxoa3NhZUowCjhVZ2pQOVVtTVlyaFZKdTFqY0ZXVjdmei9yKzIxL2F3VG5EVjlzTVlRcXVJUllZeTdiRzByMU9iaXdkb3ZudGsKN2dGSTA2WjB2WmFjREU1Ym9xVUNBd0VBQWFPQ0FTVXdnZ0VoTUFrR0ExVWRFd1FDTUFBd0N3WURWUjBQQkFRRApBZ1VnTUIwR0ExVWREZ1FXQkJTUk9OOEdKOG8rOGpnRnRqa3R3WmRxeDZCUnlUQVRCZ05WSFNVRUREQUtCZ2dyCkJnRUZCUWNEQVRBZEJnbGdoa2dCaHZoQ0FRMEVFQllPVkdWemRDQllOVEE1SUdObGNuUXdnYk1HQTFVZEl3U0IKcXpDQnFJQVVrVGpmQmlmS1B2STRCYlk1TGNHWGFzZWdVY21oZ1l5a2dZa3dnWVl4Q3pBSkJnTlZCQVlUQWtGVgpNUXd3Q2dZRFZRUUlFd05PVTFjeER6QU5CZ05WQkFjVEJsTjVaRzVsZVRFTU1Bb0dBMVVFQ2d3RFVFbFVNUWt3CkJ3WURWUVFMREFBeEdEQVdCZ05WQkFNTUQyeGhkM0psYm1ObGNHbDBMbU52YlRFbE1DTUdDU3FHU0liM0RRRUoKQVF3V2JHRjNjbVZ1WTJVdWNHbDBRR2R0WVdsc0xtTnZiWUlCQVRBTkJna3Foa2lHOXcwQkFRc0ZBQU9CZ1FDRQpUQWVKVERTQVc2ejFVRlRWN1FyZWg0VUxGT1JhajkrZUN1RjNLV0RIYyswSVFDajlyZG5ERzRRL3dmNy9yYVEwCkpuUFFDU0NkclBMSmV5b1BIN1FhVHdvYUY3ZHpWdzRMQ3N5TkpURld4NGNNNTBWdzZSNWZET2dpQzhic2ZmUzgKQkptb3VscnJaRE5OVmpHOG1XNmNMeHJZdlZRT3JSVmVjQ0ZJZ3NzQ2JBPT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=
+
+
+
+
+
+
+ LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURxekNDQXhTZ0F3SUJBZ0lCQVRBTkJna3Foa2lHOXcwQkFRc0ZBRENCaGpFTE1Ba0dBMVVFQmhNQ1FWVXgKRERBS0JnTlZCQWdUQTA1VFZ6RVBNQTBHQTFVRUJ4TUdVM2xrYm1WNU1Rd3dDZ1lEVlFRS0RBTlFTVlF4Q1RBSApCZ05WQkFzTUFERVlNQllHQTFVRUF3d1BiR0YzY21WdVkyVndhWFF1WTI5dE1TVXdJd1lKS29aSWh2Y05BUWtCCkRCWnNZWGR5Wlc1alpTNXdhWFJBWjIxaGFXd3VZMjl0TUI0WERURXlNRFF4T1RJeU5UUXhPRm9YRFRNeU1EUXgKTkRJeU5UUXhPRm93Z1lZeEN6QUpCZ05WQkFZVEFrRlZNUXd3Q2dZRFZRUUlFd05PVTFjeER6QU5CZ05WQkFjVApCbE41Wkc1bGVURU1NQW9HQTFVRUNnd0RVRWxVTVFrd0J3WURWUVFMREFBeEdEQVdCZ05WQkFNTUQyeGhkM0psCmJtTmxjR2wwTG1OdmJURWxNQ01HQ1NxR1NJYjNEUUVKQVF3V2JHRjNjbVZ1WTJVdWNHbDBRR2R0WVdsc0xtTnYKYlRDQm56QU5CZ2txaGtpRzl3MEJBUUVGQUFPQmpRQXdnWWtDZ1lFQXFqaWUzUjJvaStwRGFldndJeXMvbWJVVApubkdsa3h0ZGlrcnExMXZleHd4SmlQTmhtaHFSVzNtVXVKRXpsbElkVkw2RW14R1lUcXBxZjkzSGxoa3NhZUowCjhVZ2pQOVVtTVlyaFZKdTFqY0ZXVjdmei9yKzIxL2F3VG5EVjlzTVlRcXVJUllZeTdiRzByMU9iaXdkb3ZudGsKN2dGSTA2WjB2WmFjREU1Ym9xVUNBd0VBQWFPQ0FTVXdnZ0VoTUFrR0ExVWRFd1FDTUFBd0N3WURWUjBQQkFRRApBZ1VnTUIwR0ExVWREZ1FXQkJTUk9OOEdKOG8rOGpnRnRqa3R3WmRxeDZCUnlUQVRCZ05WSFNVRUREQUtCZ2dyCkJnRUZCUWNEQVRBZEJnbGdoa2dCaHZoQ0FRMEVFQllPVkdWemRDQllOVEE1SUdObGNuUXdnYk1HQTFVZEl3U0IKcXpDQnFJQVVrVGpmQmlmS1B2STRCYlk1TGNHWGFzZWdVY21oZ1l5a2dZa3dnWVl4Q3pBSkJnTlZCQVlUQWtGVgpNUXd3Q2dZRFZRUUlFd05PVTFjeER6QU5CZ05WQkFjVEJsTjVaRzVsZVRFTU1Bb0dBMVVFQ2d3RFVFbFVNUWt3CkJ3WURWUVFMREFBeEdEQVdCZ05WQkFNTUQyeGhkM0psYm1ObGNHbDBMbU52YlRFbE1DTUdDU3FHU0liM0RRRUoKQVF3V2JHRjNjbVZ1WTJVdWNHbDBRR2R0WVdsc0xtTnZiWUlCQVRBTkJna3Foa2lHOXcwQkFRc0ZBQU9CZ1FDRQpUQWVKVERTQVc2ejFVRlRWN1FyZWg0VUxGT1JhajkrZUN1RjNLV0RIYyswSVFDajlyZG5ERzRRL3dmNy9yYVEwCkpuUFFDU0NkclBMSmV5b1BIN1FhVHdvYUY3ZHpWdzRMQ3N5TkpURld4NGNNNTBWdzZSNWZET2dpQzhic2ZmUzgKQkptb3VscnJaRE5OVmpHOG1XNmNMeHJZdlZRT3JSVmVjQ0ZJZ3NzQ2JBPT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=
+
+
+
+
+ urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified
+ urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress
+ urn:oasis:names:tc:SAML:2.0:nameid-format:persistent
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py b/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py
index a9a46908..1c70f1b2 100644
--- a/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py
+++ b/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py
@@ -273,6 +273,44 @@ def test_parse_required_binding_all(self):
self.assertEqual(expected_settings6_7, settings6)
self.assertEqual(expected_settings6_7, settings7)
+ def test_parse_with_entity_id(self):
+ """
+ Tests the parse method of the OneLogin_Saml2_IdPMetadataParser
+ Case: Provide entity_id to identify the desired IdPDescriptor from
+ EntitiesDescriptor
+ """
+ xml_idp_metadata = self.file_contents(join(self.data_path, 'metadata', 'idp_multiple_descriptors.xml'))
+
+ # should find first descriptor
+ data = OneLogin_Saml2_IdPMetadataParser.parse(xml_idp_metadata)
+ self.assertEqual("https://foo.example.com/access/saml/idp.xml", data["idp"]["entityId"])
+
+ # should find desired descriptor
+ data2 = OneLogin_Saml2_IdPMetadataParser.parse(xml_idp_metadata, entity_id="https://bar.example.com/access/saml/idp.xml")
+ self.assertEqual("https://bar.example.com/access/saml/idp.xml", data2["idp"]["entityId"])
+
+ expected_settings_json = """
+ {
+ "sp": {
+ "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"
+ },
+ "idp": {
+ "singleLogoutService": {
+ "url": "https://hello.example.com/access/saml/logout",
+ "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
+ },
+ "entityId": "https://bar.example.com/access/saml/idp.xml",
+ "x509cert": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURxekNDQXhTZ0F3SUJBZ0lCQVRBTkJna3Foa2lHOXcwQkFRc0ZBRENCaGpFTE1Ba0dBMVVFQmhNQ1FWVXgKRERBS0JnTlZCQWdUQTA1VFZ6RVBNQTBHQTFVRUJ4TUdVM2xrYm1WNU1Rd3dDZ1lEVlFRS0RBTlFTVlF4Q1RBSApCZ05WQkFzTUFERVlNQllHQTFVRUF3d1BiR0YzY21WdVkyVndhWFF1WTI5dE1TVXdJd1lKS29aSWh2Y05BUWtCCkRCWnNZWGR5Wlc1alpTNXdhWFJBWjIxaGFXd3VZMjl0TUI0WERURXlNRFF4T1RJeU5UUXhPRm9YRFRNeU1EUXgKTkRJeU5UUXhPRm93Z1lZeEN6QUpCZ05WQkFZVEFrRlZNUXd3Q2dZRFZRUUlFd05PVTFjeER6QU5CZ05WQkFjVApCbE41Wkc1bGVURU1NQW9HQTFVRUNnd0RVRWxVTVFrd0J3WURWUVFMREFBeEdEQVdCZ05WQkFNTUQyeGhkM0psCmJtTmxjR2wwTG1OdmJURWxNQ01HQ1NxR1NJYjNEUUVKQVF3V2JHRjNjbVZ1WTJVdWNHbDBRR2R0WVdsc0xtTnYKYlRDQm56QU5CZ2txaGtpRzl3MEJBUUVGQUFPQmpRQXdnWWtDZ1lFQXFqaWUzUjJvaStwRGFldndJeXMvbWJVVApubkdsa3h0ZGlrcnExMXZleHd4SmlQTmhtaHFSVzNtVXVKRXpsbElkVkw2RW14R1lUcXBxZjkzSGxoa3NhZUowCjhVZ2pQOVVtTVlyaFZKdTFqY0ZXVjdmei9yKzIxL2F3VG5EVjlzTVlRcXVJUllZeTdiRzByMU9iaXdkb3ZudGsKN2dGSTA2WjB2WmFjREU1Ym9xVUNBd0VBQWFPQ0FTVXdnZ0VoTUFrR0ExVWRFd1FDTUFBd0N3WURWUjBQQkFRRApBZ1VnTUIwR0ExVWREZ1FXQkJTUk9OOEdKOG8rOGpnRnRqa3R3WmRxeDZCUnlUQVRCZ05WSFNVRUREQUtCZ2dyCkJnRUZCUWNEQVRBZEJnbGdoa2dCaHZoQ0FRMEVFQllPVkdWemRDQllOVEE1SUdObGNuUXdnYk1HQTFVZEl3U0IKcXpDQnFJQVVrVGpmQmlmS1B2STRCYlk1TGNHWGFzZWdVY21oZ1l5a2dZa3dnWVl4Q3pBSkJnTlZCQVlUQWtGVgpNUXd3Q2dZRFZRUUlFd05PVTFjeER6QU5CZ05WQkFjVEJsTjVaRzVsZVRFTU1Bb0dBMVVFQ2d3RFVFbFVNUWt3CkJ3WURWUVFMREFBeEdEQVdCZ05WQkFNTUQyeGhkM0psYm1ObGNHbDBMbU52YlRFbE1DTUdDU3FHU0liM0RRRUoKQVF3V2JHRjNjbVZ1WTJVdWNHbDBRR2R0WVdsc0xtTnZiWUlCQVRBTkJna3Foa2lHOXcwQkFRc0ZBQU9CZ1FDRQpUQWVKVERTQVc2ejFVRlRWN1FyZWg0VUxGT1JhajkrZUN1RjNLV0RIYyswSVFDajlyZG5ERzRRL3dmNy9yYVEwCkpuUFFDU0NkclBMSmV5b1BIN1FhVHdvYUY3ZHpWdzRMQ3N5TkpURld4NGNNNTBWdzZSNWZET2dpQzhic2ZmUzgKQkptb3VscnJaRE5OVmpHOG1XNmNMeHJZdlZRT3JSVmVjQ0ZJZ3NzQ2JBPT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=",
+ "singleSignOnService": {
+ "url": "https://hello.example.com/access/saml/login",
+ "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
+ }
+ }
+ }
+ """
+ expected_settings = json.loads(expected_settings_json)
+ self.assertEqual(expected_settings, data2)
+
def testMergeSettings(self):
"""
Tests the merge_settings method of the OneLogin_Saml2_IdPMetadataParser
From f267167d0f795c30b4ccb79bc719e86e23842b64 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Fri, 12 May 2017 19:21:57 +0200
Subject: [PATCH 104/255] Adapt IdP XML metadata parser to take care of
multiple IdP certtificates and be able to inject the data obtained on the
settings
---
src/onelogin/saml2/idp_metadata_parser.py | 39 +++++-
.../metadata/idp_metadata_multi_certs.xml | 75 ++++++++++
.../saml2_tests/idp_metadata_parser_test.py | 131 ++++++++++++++++--
3 files changed, 227 insertions(+), 18 deletions(-)
create mode 100644 tests/data/metadata/idp_metadata_multi_certs.xml
diff --git a/src/onelogin/saml2/idp_metadata_parser.py b/src/onelogin/saml2/idp_metadata_parser.py
index 28154820..d4c69015 100644
--- a/src/onelogin/saml2/idp_metadata_parser.py
+++ b/src/onelogin/saml2/idp_metadata_parser.py
@@ -126,7 +126,7 @@ def parse(
entity_descriptor_nodes = OneLogin_Saml2_Utils.query(dom, entity_desc_path)
- idp_entity_id = want_authn_requests_signed = idp_name_id_format = idp_sso_url = idp_slo_url = idp_x509_cert = None
+ idp_entity_id = want_authn_requests_signed = idp_name_id_format = idp_sso_url = idp_slo_url = certs = None
if len(entity_descriptor_nodes) > 0:
for entity_descriptor_node in entity_descriptor_nodes:
@@ -157,9 +157,19 @@ def parse(
if len(slo_nodes) > 0:
idp_slo_url = slo_nodes[0].get('Location', None)
- cert_nodes = OneLogin_Saml2_Utils.query(idp_descriptor_node, "./md:KeyDescriptor[@use='signing']/ds:KeyInfo/ds:X509Data/ds:X509Certificate")
- if len(cert_nodes) > 0:
- idp_x509_cert = cert_nodes[0].text
+ signing_nodes = OneLogin_Saml2_Utils.query(idp_descriptor_node, "./md:KeyDescriptor[not(contains(@use, 'signing'))]/ds:KeyInfo/ds:X509Data/ds:X509Certificate")
+ encryption_nodes = OneLogin_Saml2_Utils.query(idp_descriptor_node, "./md:KeyDescriptor[not(contains(@use, 'encryption'))]/ds:KeyInfo/ds:X509Data/ds:X509Certificate")
+
+ if len(signing_nodes) > 0 or len(encryption_nodes) > 0:
+ certs = {}
+ if len(signing_nodes) > 0:
+ certs['signing'] = []
+ for cert_node in signing_nodes:
+ certs['signing'].append(''.join(cert_node.text.split()))
+ if len(encryption_nodes) > 0:
+ certs['encryption'] = []
+ for cert_node in encryption_nodes:
+ certs['encryption'].append(''.join(cert_node.text.split()))
data['idp'] = {}
@@ -176,8 +186,17 @@ def parse(
data['idp']['singleLogoutService']['url'] = idp_slo_url
data['idp']['singleLogoutService']['binding'] = required_slo_binding
- if idp_x509_cert is not None:
- data['idp']['x509cert'] = idp_x509_cert
+ if certs is not None:
+ if len(certs) == 1 or \
+ (('signing' in certs and len(certs['signing']) == 1) and
+ ('encryption' in certs and len(certs['encryption']) == 1 and
+ certs['signing'][0] == certs['encryption'][0])):
+ if 'signing' in certs:
+ data['idp']['x509cert'] = certs['signing'][0]
+ else:
+ data['idp']['x509cert'] = certs['encryption'][0]
+ else:
+ data['idp']['x509certMulti'] = certs
if want_authn_requests_signed is not None:
data['security'] = {}
@@ -211,6 +230,14 @@ def merge_settings(settings, new_metadata_settings):
# Guarantee to not modify original data (`settings.copy()` would not
# be sufficient, as it's just a shallow copy).
result_settings = deepcopy(settings)
+
+ # previously I will take care of cert stuff
+ if 'idp' in new_metadata_settings and 'idp' in result_settings:
+ if new_metadata_settings['idp'].get('x509cert', None) and result_settings['idp'].get('x509certMulti',None):
+ del result_settings['idp']['x509certMulti']
+ if new_metadata_settings['idp'].get('x509certMulti', None) and result_settings['idp'].get('x509cert',None):
+ del result_settings['idp']['x509cert']
+
# Merge `new_metadata_settings` into `result_settings`.
dict_deep_merge(result_settings, new_metadata_settings)
return result_settings
diff --git a/tests/data/metadata/idp_metadata_multi_certs.xml b/tests/data/metadata/idp_metadata_multi_certs.xml
new file mode 100644
index 00000000..f993f64a
--- /dev/null
+++ b/tests/data/metadata/idp_metadata_multi_certs.xml
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+ MIIEZTCCA02gAwIBAgIUPyy/A3bZAZ4m28PzEUUoT7RJhxIwDQYJKoZIhvcNAQEF
+BQAwcjELMAkGA1UEBhMCVVMxKzApBgNVBAoMIk9uZUxvZ2luIFRlc3QgKHNnYXJj
+aWEtdXMtcHJlcHJvZCkxFTATBgNVBAsMDE9uZUxvZ2luIElkUDEfMB0GA1UEAwwW
+T25lTG9naW4gQWNjb3VudCA4OTE0NjAeFw0xNjA4MDQyMjI5MzdaFw0yMTA4MDUy
+MjI5MzdaMHIxCzAJBgNVBAYTAlVTMSswKQYDVQQKDCJPbmVMb2dpbiBUZXN0IChz
+Z2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxHzAdBgNV
+BAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDYwggEiMA0GCSqGSIb3DQEBAQUAA4IB
+DwAwggEKAoIBAQDN6iqQGcLOCglNO42I2rkzE05UXSiMXT6c8ALThMMiaDw6qqzo
+3sd/tKK+NcNKWLIIC8TozWVyh5ykUiVZps+08xil7VsTU7E+wKu3kvmOsvw2wlRw
+tnoKZJwYhnr+RkBa+h1r3ZYUgXm1ZPeHMKj1g18KaWz9+MxYL6BhKqrOzfW/P2xx
+VRcFH7/pq+ZsDdgNzD2GD+apzY4MZyZj/N6BpBWJ0GlFsmtBegpbX3LBitJuFkk5
+L4/U/jjF1AJa3boBdCUVfATqO5G03H4XS1GySjBIRQXmlUF52rLjg6xCgWJ30/+t
+1X+IHLJeixiQ0vxyh6C4/usCEt94cgD1r8ADAgMBAAGjgfIwge8wDAYDVR0TAQH/
+BAIwADAdBgNVHQ4EFgQUPW0DcH0G3IwynWgi74co4wZ6n7gwga8GA1UdIwSBpzCB
+pIAUPW0DcH0G3IwynWgi74co4wZ6n7ihdqR0MHIxCzAJBgNVBAYTAlVTMSswKQYD
+VQQKDCJPbmVMb2dpbiBUZXN0IChzZ2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQL
+DAxPbmVMb2dpbiBJZFAxHzAdBgNVBAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDaC
+FD8svwN22QGeJtvD8xFFKE+0SYcSMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0B
+AQUFAAOCAQEAQhB4q9jrycwbHrDSoYR1X4LFFzvJ9Us75wQquRHXpdyS9D6HUBXM
+GI6ahPicXCQrfLgN8vzMIiqZqfySXXv/8/dxe/X4UsWLYKYJHDJmxXD5EmWTa65c
+hjkeP1oJAc8f3CKCpcP2lOBTthbnk2fEVAeLHR4xNdQO0VvGXWO9BliYPpkYqUIB
+vlm+Fg9mF7AM/Uagq2503XXIE1Lq//HON68P10vNMwLSKOtYLsoTiCnuIKGJqG37
+MsZVjQ1ZPRcO+LSLkq0i91gFxrOrVCrgztX4JQi5XkvEsYZGIXXjwHqxTVyt3adZ
+WQO0LPxPqRiUqUzyhDhLo/xXNrHCu4VbMw==
+
+
+
+
+
+
+ MIICZDCCAc2gAwIBAgIBADANBgkqhkiG9w0BAQ0FADBPMQswCQYDVQQGEwJ1czEUMBIGA1UECAwLZXhhbXBsZS5jb20xFDASBgNVBAoMC2V4YW1wbGUuY29tMRQwEgYDVQQDDAtleGFtcGxlLmNvbTAeFw0xNzA0MTUxNjMzMThaFw0xODA0MTUxNjMzMThaME8xCzAJBgNVBAYTAnVzMRQwEgYDVQQIDAtleGFtcGxlLmNvbTEUMBIGA1UECgwLZXhhbXBsZS5jb20xFDASBgNVBAMMC2V4YW1wbGUuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC6GLkl5lDUZdHNDAojp5i24OoPlqrt5TGXJIPqAZYT1hQvJW5nv17MFDHrjmtEnmW4ACKEy0fAX80QWIcHunZSkbEGHb+NG/6oTi5RipXMvmHnfFnPJJ0AdtiLiPE478CV856gXekV4Xx5u3KrylcOgkpYsp0GMIQBDzleMUXlYQIDAQABo1AwTjAdBgNVHQ4EFgQUnP8vlYPGPL2n6ZzDYij2kMDC8wMwHwYDVR0jBBgwFoAUnP8vlYPGPL2n6ZzDYij2kMDC8wMwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQ0FAAOBgQAlQGAl+b8Cpot1g+65lLLjVoY7APJPWLW0klKQNlMU0s4MU+71Y3ExUEOXDAZgKcFoavb1fEOGMwEf38NaJAy1e/l6VNuixXShffq20ymqHQxOG0q8ujeNkgZF9k6XDfn/QZ3AD0o/IrCT7UMc/0QsfgIjWYxwCvp2syApc5CYfQ==
+
+
+
+
+
+
+ MIIEZTCCA02gAwIBAgIUPyy/A3bZAZ4m28PzEUUoT7RJhxIwDQYJKoZIhvcNAQEF
+BQAwcjELMAkGA1UEBhMCVVMxKzApBgNVBAoMIk9uZUxvZ2luIFRlc3QgKHNnYXJj
+aWEtdXMtcHJlcHJvZCkxFTATBgNVBAsMDE9uZUxvZ2luIElkUDEfMB0GA1UEAwwW
+T25lTG9naW4gQWNjb3VudCA4OTE0NjAeFw0xNjA4MDQyMjI5MzdaFw0yMTA4MDUy
+MjI5MzdaMHIxCzAJBgNVBAYTAlVTMSswKQYDVQQKDCJPbmVMb2dpbiBUZXN0IChz
+Z2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxHzAdBgNV
+BAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDYwggEiMA0GCSqGSIb3DQEBAQUAA4IB
+DwAwggEKAoIBAQDN6iqQGcLOCglNO42I2rkzE05UXSiMXT6c8ALThMMiaDw6qqzo
+3sd/tKK+NcNKWLIIC8TozWVyh5ykUiVZps+08xil7VsTU7E+wKu3kvmOsvw2wlRw
+tnoKZJwYhnr+RkBa+h1r3ZYUgXm1ZPeHMKj1g18KaWz9+MxYL6BhKqrOzfW/P2xx
+VRcFH7/pq+ZsDdgNzD2GD+apzY4MZyZj/N6BpBWJ0GlFsmtBegpbX3LBitJuFkk5
+L4/U/jjF1AJa3boBdCUVfATqO5G03H4XS1GySjBIRQXmlUF52rLjg6xCgWJ30/+t
+1X+IHLJeixiQ0vxyh6C4/usCEt94cgD1r8ADAgMBAAGjgfIwge8wDAYDVR0TAQH/
+BAIwADAdBgNVHQ4EFgQUPW0DcH0G3IwynWgi74co4wZ6n7gwga8GA1UdIwSBpzCB
+pIAUPW0DcH0G3IwynWgi74co4wZ6n7ihdqR0MHIxCzAJBgNVBAYTAlVTMSswKQYD
+VQQKDCJPbmVMb2dpbiBUZXN0IChzZ2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQL
+DAxPbmVMb2dpbiBJZFAxHzAdBgNVBAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDaC
+FD8svwN22QGeJtvD8xFFKE+0SYcSMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0B
+AQUFAAOCAQEAQhB4q9jrycwbHrDSoYR1X4LFFzvJ9Us75wQquRHXpdyS9D6HUBXM
+GI6ahPicXCQrfLgN8vzMIiqZqfySXXv/8/dxe/X4UsWLYKYJHDJmxXD5EmWTa65c
+hjkeP1oJAc8f3CKCpcP2lOBTthbnk2fEVAeLHR4xNdQO0VvGXWO9BliYPpkYqUIB
+vlm+Fg9mF7AM/Uagq2503XXIE1Lq//HON68P10vNMwLSKOtYLsoTiCnuIKGJqG37
+MsZVjQ1ZPRcO+LSLkq0i91gFxrOrVCrgztX4JQi5XkvEsYZGIXXjwHqxTVyt3adZ
+WQO0LPxPqRiUqUzyhDhLo/xXNrHCu4VbMw==
+
+
+
+
+ urn:oasis:names:tc:SAML:2.0:nameid-format:transient
+
+
+
\ No newline at end of file
diff --git a/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py b/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py
index 1c70f1b2..53e9b2b0 100644
--- a/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py
+++ b/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py
@@ -71,19 +71,21 @@ def testParseRemote(self):
self.assertTrue(data is not None and data is not {})
expected_settings_json = """
{
- "sp": {
- "NameIDFormat": "urn:mace:shibboleth:1.0:nameIdentifier"
- },
- "idp": {
- "entityId": "https://idp.testshib.org/idp/shibboleth",
- "singleSignOnService": {
- "url": "https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO",
- "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
+ "sp": {
+ "NameIDFormat": "urn:mace:shibboleth:1.0:nameIdentifier"
+ },
+ "idp": {
+ "x509cert": "MIIDAzCCAeugAwIBAgIVAPX0G6LuoXnKS0Muei006mVSBXbvMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNVBAMMEGlkcC50ZXN0c2hpYi5vcmcwHhcNMTYwODIzMjEyMDU0WhcNMzYwODIzMjEyMDU0WjAbMRkwFwYDVQQDDBBpZHAudGVzdHNoaWIub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAg9C4J2DiRTEhJAWzPt1S3ryhm3M2P3hPpwJwvt2q948vdTUxhhvNMuc3M3S4WNh6JYBs53R+YmjqJAII4ShMGNEmlGnSVfHorex7IxikpuDPKV3SNf28mCAZbQrX+hWA+ann/uifVzqXktOjs6DdzdBnxoVhniXgC8WCJwKcx6JO/hHsH1rG/0DSDeZFpTTcZHj4S9MlLNUtt5JxRzV/MmmB3ObaX0CMqsSWUOQeE4nylSlp5RWHCnx70cs9kwz5WrflnbnzCeHU2sdbNotBEeTHot6a2cj/pXlRJIgPsrL/4VSicPZcGYMJMPoLTJ8mdy6mpR6nbCmP7dVbCIm/DQIDAQABoz4wPDAdBgNVHQ4EFgQUUfaDa2mPi24x09yWp1OFXmZ2GPswGwYDVR0RBBQwEoIQaWRwLnRlc3RzaGliLm9yZzANBgkqhkiG9w0BAQsFAAOCAQEASKKgqTxhqBzROZ1eVy++si+eTTUQZU4+8UywSKLia2RattaAPMAcXUjO+3cYOQXLVASdlJtt+8QPdRkfp8SiJemHPXC8BES83pogJPYEGJsKo19l4XFJHPnPy+Dsn3mlJyOfAa8RyWBS80u5lrvAcr2TJXt9fXgkYs7BOCigxtZoR8flceGRlAZ4p5FPPxQR6NDYb645jtOTMVr3zgfjP6Wh2dt+2p04LG7ENJn8/gEwtXVuXCsPoSCDx9Y0QmyXTJNdV1aB0AhORkWPlFYwp+zOyOIR+3m1+pqWFpn0eT/HrxpdKa74FA3R2kq4R7dXe4G0kUgXTdqXMLRKhDgdmA==",
+ "entityId": "https://idp.testshib.org/idp/shibboleth",
+ "singleSignOnService": {
+ "url": "https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO",
+ "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
+ }
}
- }
}
"""
expected_settings = json.loads(expected_settings_json)
+
self.assertEqual(expected_settings, data)
def testParse(self):
@@ -109,7 +111,7 @@ def testParse(self):
"url": "https://app.onelogin.com/trust/saml2/http-post/sso/383123",
"binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
},
- "x509cert": "MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzET\\nMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREwDwYD\\nVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbTAeFw0xMzA2\\nMDUxNzE2MjBaFw0xODA2MDUxNzE2MjBaMGcxCzAJBgNVBAYTAlVTMRMwEQYDVQQI\\nDApDYWxpZm9ybmlhMRUwEwYDVQQHDAxTYW50YSBNb25pY2ExETAPBgNVBAoMCE9u\\nZUxvZ2luMRkwFwYDVQQDDBBhcHAub25lbG9naW4uY29tMIIBIjANBgkqhkiG9w0B\\nAQEFAAOCAQ8AMIIBCgKCAQEAse8rnep4qL2GmhH10pMQyJ2Jae+AQHyfgVjaQZ7Z\\n0QQog5jX91vcJRSMi0XWJnUtOr6lF0dq1+yckjZ92wyLrH+7fvngNO1aV4Mjk9sT\\ngf+iqMrae6y6fRxDt9PXrEFVjvd3vv7QTJf2FuIPy4vVP06Dt8EMkQIr8rmLmU0m\\nTr1k2DkrdtdlCuNFTXuAu3QqfvNCRrRwfNObn9MP6JeOUdcGLJsBjGF8exfcN1SF\\nzRF0JFr3dmOlx761zK5liD0T1sYWnDquatj/JD9fZMbKecBKni1NglH/LVd+b6aJ\\nUAr5LulERULUjLqYJRKW31u91/4Qazdo9tbvwqyFxaoUrwIDAQABo4HUMIHRMAwG\\nA1UdEwEB/wQCMAAwHQYDVR0OBBYEFPWcXvQSlTXnzZD2xziuoUvrrDedMIGRBgNV\\nHSMEgYkwgYaAFPWcXvQSlTXnzZD2xziuoUvrrDedoWukaTBnMQswCQYDVQQGEwJV\\nUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREw\\nDwYDVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbYIBATAO\\nBgNVHQ8BAf8EBAMCBPAwDQYJKoZIhvcNAQEFBQADggEBAB/8xe3rzqXQVxzHyAHu\\nAuPa73ClDoL1cko0Fp8CGcqEIyj6Te9gx5z6wyfv+Lo8RFvBLlnB1lXqbC+fTGcV\\ngG/4oKLJ5UwRFxInqpZPnOAudVNnd0PYOODn9FWs6u+OTIQIaIcPUv3MhB9lwHIJ\\nsTk/bs9xcru5TPyLIxLLd6ib/pRceKH2mTkzUd0DYk9CQNXXeoGx/du5B9nh3ClP\\nTbVakRzl3oswgI5MQIphYxkW70SopEh4kOFSRE1ND31NNIq1YrXlgtkguQBFsZWu\\nQOPR6cEwFZzP0tHTYbI839WgxX6hfhIUTUz6mLqq4+3P4BG3+1OXeVDg63y8Uh78\\n1sE=",
+ "x509cert": "MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREwDwYDVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbTAeFw0xMzA2MDUxNzE2MjBaFw0xODA2MDUxNzE2MjBaMGcxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRUwEwYDVQQHDAxTYW50YSBNb25pY2ExETAPBgNVBAoMCE9uZUxvZ2luMRkwFwYDVQQDDBBhcHAub25lbG9naW4uY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAse8rnep4qL2GmhH10pMQyJ2Jae+AQHyfgVjaQZ7Z0QQog5jX91vcJRSMi0XWJnUtOr6lF0dq1+yckjZ92wyLrH+7fvngNO1aV4Mjk9sTgf+iqMrae6y6fRxDt9PXrEFVjvd3vv7QTJf2FuIPy4vVP06Dt8EMkQIr8rmLmU0mTr1k2DkrdtdlCuNFTXuAu3QqfvNCRrRwfNObn9MP6JeOUdcGLJsBjGF8exfcN1SFzRF0JFr3dmOlx761zK5liD0T1sYWnDquatj/JD9fZMbKecBKni1NglH/LVd+b6aJUAr5LulERULUjLqYJRKW31u91/4Qazdo9tbvwqyFxaoUrwIDAQABo4HUMIHRMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFPWcXvQSlTXnzZD2xziuoUvrrDedMIGRBgNVHSMEgYkwgYaAFPWcXvQSlTXnzZD2xziuoUvrrDedoWukaTBnMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREwDwYDVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbYIBATAOBgNVHQ8BAf8EBAMCBPAwDQYJKoZIhvcNAQEFBQADggEBAB/8xe3rzqXQVxzHyAHuAuPa73ClDoL1cko0Fp8CGcqEIyj6Te9gx5z6wyfv+Lo8RFvBLlnB1lXqbC+fTGcVgG/4oKLJ5UwRFxInqpZPnOAudVNnd0PYOODn9FWs6u+OTIQIaIcPUv3MhB9lwHIJsTk/bs9xcru5TPyLIxLLd6ib/pRceKH2mTkzUd0DYk9CQNXXeoGx/du5B9nh3ClPTbVakRzl3oswgI5MQIphYxkW70SopEh4kOFSRE1ND31NNIq1YrXlgtkguQBFsZWuQOPR6cEwFZzP0tHTYbI839WgxX6hfhIUTUz6mLqq4+3P4BG3+1OXeVDg63y8Uh781sE=",
"entityId": "https://app.onelogin.com/saml/metadata/383123"
},
"sp": {
@@ -137,7 +139,8 @@ def test_parse_testshib_required_binding_sso_redirect(self):
"singleSignOnService": {
"url": "https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO",
"binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
- }
+ },
+ "x509cert": "MIIDAzCCAeugAwIBAgIVAPX0G6LuoXnKS0Muei006mVSBXbvMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNVBAMMEGlkcC50ZXN0c2hpYi5vcmcwHhcNMTYwODIzMjEyMDU0WhcNMzYwODIzMjEyMDU0WjAbMRkwFwYDVQQDDBBpZHAudGVzdHNoaWIub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAg9C4J2DiRTEhJAWzPt1S3ryhm3M2P3hPpwJwvt2q948vdTUxhhvNMuc3M3S4WNh6JYBs53R+YmjqJAII4ShMGNEmlGnSVfHorex7IxikpuDPKV3SNf28mCAZbQrX+hWA+ann/uifVzqXktOjs6DdzdBnxoVhniXgC8WCJwKcx6JO/hHsH1rG/0DSDeZFpTTcZHj4S9MlLNUtt5JxRzV/MmmB3ObaX0CMqsSWUOQeE4nylSlp5RWHCnx70cs9kwz5WrflnbnzCeHU2sdbNotBEeTHot6a2cj/pXlRJIgPsrL/4VSicPZcGYMJMPoLTJ8mdy6mpR6nbCmP7dVbCIm/DQIDAQABoz4wPDAdBgNVHQ4EFgQUUfaDa2mPi24x09yWp1OFXmZ2GPswGwYDVR0RBBQwEoIQaWRwLnRlc3RzaGliLm9yZzANBgkqhkiG9w0BAQsFAAOCAQEASKKgqTxhqBzROZ1eVy++si+eTTUQZU4+8UywSKLia2RattaAPMAcXUjO+3cYOQXLVASdlJtt+8QPdRkfp8SiJemHPXC8BES83pogJPYEGJsKo19l4XFJHPnPy+Dsn3mlJyOfAa8RyWBS80u5lrvAcr2TJXt9fXgkYs7BOCigxtZoR8flceGRlAZ4p5FPPxQR6NDYb645jtOTMVr3zgfjP6Wh2dt+2p04LG7ENJn8/gEwtXVuXCsPoSCDx9Y0QmyXTJNdV1aB0AhORkWPlFYwp+zOyOIR+3m1+pqWFpn0eT/HrxpdKa74FA3R2kq4R7dXe4G0kUgXTdqXMLRKhDgdmA=="
}
}
"""
@@ -169,6 +172,7 @@ def test_parse_testshib_required_binding_sso_post(self):
"NameIDFormat": "urn:mace:shibboleth:1.0:nameIdentifier"
},
"idp": {
+ "x509cert": "MIIDAzCCAeugAwIBAgIVAPX0G6LuoXnKS0Muei006mVSBXbvMA0GCSqGSIb3DQEBCwUAMBsxGTAXBgNVBAMMEGlkcC50ZXN0c2hpYi5vcmcwHhcNMTYwODIzMjEyMDU0WhcNMzYwODIzMjEyMDU0WjAbMRkwFwYDVQQDDBBpZHAudGVzdHNoaWIub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAg9C4J2DiRTEhJAWzPt1S3ryhm3M2P3hPpwJwvt2q948vdTUxhhvNMuc3M3S4WNh6JYBs53R+YmjqJAII4ShMGNEmlGnSVfHorex7IxikpuDPKV3SNf28mCAZbQrX+hWA+ann/uifVzqXktOjs6DdzdBnxoVhniXgC8WCJwKcx6JO/hHsH1rG/0DSDeZFpTTcZHj4S9MlLNUtt5JxRzV/MmmB3ObaX0CMqsSWUOQeE4nylSlp5RWHCnx70cs9kwz5WrflnbnzCeHU2sdbNotBEeTHot6a2cj/pXlRJIgPsrL/4VSicPZcGYMJMPoLTJ8mdy6mpR6nbCmP7dVbCIm/DQIDAQABoz4wPDAdBgNVHQ4EFgQUUfaDa2mPi24x09yWp1OFXmZ2GPswGwYDVR0RBBQwEoIQaWRwLnRlc3RzaGliLm9yZzANBgkqhkiG9w0BAQsFAAOCAQEASKKgqTxhqBzROZ1eVy++si+eTTUQZU4+8UywSKLia2RattaAPMAcXUjO+3cYOQXLVASdlJtt+8QPdRkfp8SiJemHPXC8BES83pogJPYEGJsKo19l4XFJHPnPy+Dsn3mlJyOfAa8RyWBS80u5lrvAcr2TJXt9fXgkYs7BOCigxtZoR8flceGRlAZ4p5FPPxQR6NDYb645jtOTMVr3zgfjP6Wh2dt+2p04LG7ENJn8/gEwtXVuXCsPoSCDx9Y0QmyXTJNdV1aB0AhORkWPlFYwp+zOyOIR+3m1+pqWFpn0eT/HrxpdKa74FA3R2kq4R7dXe4G0kUgXTdqXMLRKhDgdmA==",
"entityId": "https://idp.testshib.org/idp/shibboleth",
"singleSignOnService": {
"url": "https://idp.testshib.org/idp/profile/SAML2/POST/SSO",
@@ -311,6 +315,44 @@ def test_parse_with_entity_id(self):
expected_settings = json.loads(expected_settings_json)
self.assertEqual(expected_settings, data2)
+ def test_parse_multi_certs(self):
+ """
+ Tests the parse method of the OneLogin_Saml2_IdPMetadataParser
+ Case: IdP metadata contains multiple certs
+ """
+ xml_idp_metadata = self.file_contents(join(self.data_path, 'metadata', 'idp_metadata_multi_certs.xml'))
+ data = OneLogin_Saml2_IdPMetadataParser.parse(xml_idp_metadata)
+
+ expected_settings_json = """
+ {
+ "sp": {
+ "NameIDFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:transient"
+ },
+ "idp": {
+ "singleLogoutService": {
+ "url": "https://idp.examle.com/saml/slo",
+ "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
+ },
+ "x509certMulti": {
+ "signing": [
+ "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=="
+ ],
+ "encryption": [
+ "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==",
+ "MIICZDCCAc2gAwIBAgIBADANBgkqhkiG9w0BAQ0FADBPMQswCQYDVQQGEwJ1czEUMBIGA1UECAwLZXhhbXBsZS5jb20xFDASBgNVBAoMC2V4YW1wbGUuY29tMRQwEgYDVQQDDAtleGFtcGxlLmNvbTAeFw0xNzA0MTUxNjMzMThaFw0xODA0MTUxNjMzMThaME8xCzAJBgNVBAYTAnVzMRQwEgYDVQQIDAtleGFtcGxlLmNvbTEUMBIGA1UECgwLZXhhbXBsZS5jb20xFDASBgNVBAMMC2V4YW1wbGUuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC6GLkl5lDUZdHNDAojp5i24OoPlqrt5TGXJIPqAZYT1hQvJW5nv17MFDHrjmtEnmW4ACKEy0fAX80QWIcHunZSkbEGHb+NG/6oTi5RipXMvmHnfFnPJJ0AdtiLiPE478CV856gXekV4Xx5u3KrylcOgkpYsp0GMIQBDzleMUXlYQIDAQABo1AwTjAdBgNVHQ4EFgQUnP8vlYPGPL2n6ZzDYij2kMDC8wMwHwYDVR0jBBgwFoAUnP8vlYPGPL2n6ZzDYij2kMDC8wMwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQ0FAAOBgQAlQGAl+b8Cpot1g+65lLLjVoY7APJPWLW0klKQNlMU0s4MU+71Y3ExUEOXDAZgKcFoavb1fEOGMwEf38NaJAy1e/l6VNuixXShffq20ymqHQxOG0q8ujeNkgZF9k6XDfn/QZ3AD0o/IrCT7UMc/0QsfgIjWYxwCvp2syApc5CYfQ=="
+ ]
+ },
+ "entityId": "https://idp.examle.com/saml/metadata",
+ "singleSignOnService": {
+ "url": "https://idp.examle.com/saml/sso",
+ "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
+ }
+ }
+ }
+ """
+ expected_settings = json.loads(expected_settings_json)
+ self.assertEqual(expected_settings, data)
+
def testMergeSettings(self):
"""
Tests the merge_settings method of the OneLogin_Saml2_IdPMetadataParser
@@ -359,7 +401,7 @@ def testMergeSettings(self):
"singleLogoutService": {
"url": "http://idp.example.com/SingleLogoutService.php"
},
- "x509cert": "MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzET\\nMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREwDwYD\\nVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbTAeFw0xMzA2\\nMDUxNzE2MjBaFw0xODA2MDUxNzE2MjBaMGcxCzAJBgNVBAYTAlVTMRMwEQYDVQQI\\nDApDYWxpZm9ybmlhMRUwEwYDVQQHDAxTYW50YSBNb25pY2ExETAPBgNVBAoMCE9u\\nZUxvZ2luMRkwFwYDVQQDDBBhcHAub25lbG9naW4uY29tMIIBIjANBgkqhkiG9w0B\\nAQEFAAOCAQ8AMIIBCgKCAQEAse8rnep4qL2GmhH10pMQyJ2Jae+AQHyfgVjaQZ7Z\\n0QQog5jX91vcJRSMi0XWJnUtOr6lF0dq1+yckjZ92wyLrH+7fvngNO1aV4Mjk9sT\\ngf+iqMrae6y6fRxDt9PXrEFVjvd3vv7QTJf2FuIPy4vVP06Dt8EMkQIr8rmLmU0m\\nTr1k2DkrdtdlCuNFTXuAu3QqfvNCRrRwfNObn9MP6JeOUdcGLJsBjGF8exfcN1SF\\nzRF0JFr3dmOlx761zK5liD0T1sYWnDquatj/JD9fZMbKecBKni1NglH/LVd+b6aJ\\nUAr5LulERULUjLqYJRKW31u91/4Qazdo9tbvwqyFxaoUrwIDAQABo4HUMIHRMAwG\\nA1UdEwEB/wQCMAAwHQYDVR0OBBYEFPWcXvQSlTXnzZD2xziuoUvrrDedMIGRBgNV\\nHSMEgYkwgYaAFPWcXvQSlTXnzZD2xziuoUvrrDedoWukaTBnMQswCQYDVQQGEwJV\\nUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREw\\nDwYDVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbYIBATAO\\nBgNVHQ8BAf8EBAMCBPAwDQYJKoZIhvcNAQEFBQADggEBAB/8xe3rzqXQVxzHyAHu\\nAuPa73ClDoL1cko0Fp8CGcqEIyj6Te9gx5z6wyfv+Lo8RFvBLlnB1lXqbC+fTGcV\\ngG/4oKLJ5UwRFxInqpZPnOAudVNnd0PYOODn9FWs6u+OTIQIaIcPUv3MhB9lwHIJ\\nsTk/bs9xcru5TPyLIxLLd6ib/pRceKH2mTkzUd0DYk9CQNXXeoGx/du5B9nh3ClP\\nTbVakRzl3oswgI5MQIphYxkW70SopEh4kOFSRE1ND31NNIq1YrXlgtkguQBFsZWu\\nQOPR6cEwFZzP0tHTYbI839WgxX6hfhIUTUz6mLqq4+3P4BG3+1OXeVDg63y8Uh78\\n1sE="
+ "x509cert": "MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREwDwYDVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbTAeFw0xMzA2MDUxNzE2MjBaFw0xODA2MDUxNzE2MjBaMGcxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRUwEwYDVQQHDAxTYW50YSBNb25pY2ExETAPBgNVBAoMCE9uZUxvZ2luMRkwFwYDVQQDDBBhcHAub25lbG9naW4uY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAse8rnep4qL2GmhH10pMQyJ2Jae+AQHyfgVjaQZ7Z0QQog5jX91vcJRSMi0XWJnUtOr6lF0dq1+yckjZ92wyLrH+7fvngNO1aV4Mjk9sTgf+iqMrae6y6fRxDt9PXrEFVjvd3vv7QTJf2FuIPy4vVP06Dt8EMkQIr8rmLmU0mTr1k2DkrdtdlCuNFTXuAu3QqfvNCRrRwfNObn9MP6JeOUdcGLJsBjGF8exfcN1SFzRF0JFr3dmOlx761zK5liD0T1sYWnDquatj/JD9fZMbKecBKni1NglH/LVd+b6aJUAr5LulERULUjLqYJRKW31u91/4Qazdo9tbvwqyFxaoUrwIDAQABo4HUMIHRMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFPWcXvQSlTXnzZD2xziuoUvrrDedMIGRBgNVHSMEgYkwgYaAFPWcXvQSlTXnzZD2xziuoUvrrDedoWukaTBnMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREwDwYDVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbYIBATAOBgNVHQ8BAf8EBAMCBPAwDQYJKoZIhvcNAQEFBQADggEBAB/8xe3rzqXQVxzHyAHuAuPa73ClDoL1cko0Fp8CGcqEIyj6Te9gx5z6wyfv+Lo8RFvBLlnB1lXqbC+fTGcVgG/4oKLJ5UwRFxInqpZPnOAudVNnd0PYOODn9FWs6u+OTIQIaIcPUv3MhB9lwHIJsTk/bs9xcru5TPyLIxLLd6ib/pRceKH2mTkzUd0DYk9CQNXXeoGx/du5B9nh3ClPTbVakRzl3oswgI5MQIphYxkW70SopEh4kOFSRE1ND31NNIq1YrXlgtkguQBFsZWuQOPR6cEwFZzP0tHTYbI839WgxX6hfhIUTUz6mLqq4+3P4BG3+1OXeVDg63y8Uh781sE="
},
"sp": {
"NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
@@ -446,6 +488,71 @@ def testMergeSettings(self):
expected_settings2 = json.loads(expected_settings2_json)
self.assertEqual(expected_settings2, settings_result2)
+ # Test merging multiple certs
+ xml_idp_metadata = self.file_contents(join(self.data_path, 'metadata', 'idp_metadata_multi_certs.xml'))
+ data3 = OneLogin_Saml2_IdPMetadataParser.parse(xml_idp_metadata)
+ settings_result3 = OneLogin_Saml2_IdPMetadataParser.merge_settings(settings, data3)
+ expected_settings3_json = """
+ {
+ "debug": false,
+ "strict": false,
+ "custom_base_path": "../../../tests/data/customPath/",
+ "sp": {
+ "singleLogoutService": {
+ "url": "http://stuff.com/endpoints/endpoints/sls.php"
+ },
+ "assertionConsumerService": {
+ "url": "http://stuff.com/endpoints/endpoints/acs.php"
+ },
+ "entityId": "http://stuff.com/endpoints/metadata.php",
+ "NameIDFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:transient"
+ },
+ "idp": {
+ "singleLogoutService": {
+ "url": "https://idp.examle.com/saml/slo",
+ "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
+ },
+ "x509certMulti": {
+ "signing": [
+ "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=="
+ ],
+ "encryption": [
+ "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==",
+ "MIICZDCCAc2gAwIBAgIBADANBgkqhkiG9w0BAQ0FADBPMQswCQYDVQQGEwJ1czEUMBIGA1UECAwLZXhhbXBsZS5jb20xFDASBgNVBAoMC2V4YW1wbGUuY29tMRQwEgYDVQQDDAtleGFtcGxlLmNvbTAeFw0xNzA0MTUxNjMzMThaFw0xODA0MTUxNjMzMThaME8xCzAJBgNVBAYTAnVzMRQwEgYDVQQIDAtleGFtcGxlLmNvbTEUMBIGA1UECgwLZXhhbXBsZS5jb20xFDASBgNVBAMMC2V4YW1wbGUuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC6GLkl5lDUZdHNDAojp5i24OoPlqrt5TGXJIPqAZYT1hQvJW5nv17MFDHrjmtEnmW4ACKEy0fAX80QWIcHunZSkbEGHb+NG/6oTi5RipXMvmHnfFnPJJ0AdtiLiPE478CV856gXekV4Xx5u3KrylcOgkpYsp0GMIQBDzleMUXlYQIDAQABo1AwTjAdBgNVHQ4EFgQUnP8vlYPGPL2n6ZzDYij2kMDC8wMwHwYDVR0jBBgwFoAUnP8vlYPGPL2n6ZzDYij2kMDC8wMwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQ0FAAOBgQAlQGAl+b8Cpot1g+65lLLjVoY7APJPWLW0klKQNlMU0s4MU+71Y3ExUEOXDAZgKcFoavb1fEOGMwEf38NaJAy1e/l6VNuixXShffq20ymqHQxOG0q8ujeNkgZF9k6XDfn/QZ3AD0o/IrCT7UMc/0QsfgIjWYxwCvp2syApc5CYfQ=="
+ ]
+ },
+ "entityId": "https://idp.examle.com/saml/metadata",
+ "singleSignOnService": {
+ "url": "https://idp.examle.com/saml/sso",
+ "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
+ }
+ },
+ "security": {
+ "authnRequestsSigned": false,
+ "wantAssertionsSigned": false,
+ "signMetadata": false
+ },
+ "contactPerson": {
+ "technical": {
+ "emailAddress": "technical@example.com",
+ "givenName": "technical_name"
+ },
+ "support": {
+ "emailAddress": "support@example.com",
+ "givenName": "support_name"
+ }
+ },
+ "organization": {
+ "en-US": {
+ "displayname": "SP test",
+ "url": "http://sp.example.com",
+ "name": "sp_test"
+ }
+ }
+ }
+ """
+ expected_settings3 = json.loads(expected_settings3_json)
+ self.assertEqual(expected_settings3, settings_result3)
if __name__ == '__main__':
if is_running_under_teamcity():
From 21a53c36a503aef36baaf115908fec3f5102bfbf Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Fri, 12 May 2017 19:36:51 +0200
Subject: [PATCH 105/255] pep8
---
src/onelogin/saml2/idp_metadata_parser.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/onelogin/saml2/idp_metadata_parser.py b/src/onelogin/saml2/idp_metadata_parser.py
index d4c69015..a239ff93 100644
--- a/src/onelogin/saml2/idp_metadata_parser.py
+++ b/src/onelogin/saml2/idp_metadata_parser.py
@@ -233,9 +233,9 @@ def merge_settings(settings, new_metadata_settings):
# previously I will take care of cert stuff
if 'idp' in new_metadata_settings and 'idp' in result_settings:
- if new_metadata_settings['idp'].get('x509cert', None) and result_settings['idp'].get('x509certMulti',None):
+ if new_metadata_settings['idp'].get('x509cert', None) and result_settings['idp'].get('x509certMulti', None):
del result_settings['idp']['x509certMulti']
- if new_metadata_settings['idp'].get('x509certMulti', None) and result_settings['idp'].get('x509cert',None):
+ if new_metadata_settings['idp'].get('x509certMulti', None) and result_settings['idp'].get('x509cert', None):
del result_settings['idp']['x509cert']
# Merge `new_metadata_settings` into `result_settings`.
From dffcbb37b0d20bc5d985c03c752908598821e4f9 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Mon, 15 May 2017 13:02:58 +0200
Subject: [PATCH 106/255] Add method description
---
src/onelogin/saml2/idp_metadata_parser.py | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/src/onelogin/saml2/idp_metadata_parser.py b/src/onelogin/saml2/idp_metadata_parser.py
index a239ff93..5ff705b7 100644
--- a/src/onelogin/saml2/idp_metadata_parser.py
+++ b/src/onelogin/saml2/idp_metadata_parser.py
@@ -32,6 +32,9 @@ def get_metadata(url, validate_cert=True):
:param url: Url where the XML of the Identity Provider Metadata is published.
:type url: string
+ :param validate_cert: If the url uses https schema, that flag enables or not the verification of the associated certificate.
+ :type validate_cert: bool
+
:returns: metadata XML
:rtype: string
"""
@@ -67,6 +70,9 @@ def parse_remote(url, validate_cert=True, entity_id=None, **kwargs):
:param url: Url where the XML of the Identity Provider Metadata is published.
:type url: string
+ :param validate_cert: If the url uses https schema, that flag enables or not the verification of the associated certificate.
+ :type validate_cert: bool
+
:param entity_id: Specify the entity_id of the EntityDescriptor that you want to parse a XML
that contains multiple EntityDescriptor.
:type entity_id: string
From c6b86c9c2d473197200a409c0315a20ef64f8c1a Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Mon, 15 May 2017 13:34:03 +0200
Subject: [PATCH 107/255] Fix bug when retrieving signing and encryption IdP
certs
---
src/onelogin/saml2/idp_metadata_parser.py | 4 ++--
.../src/OneLogin/saml2_tests/idp_metadata_parser_test.py | 8 ++++----
2 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/src/onelogin/saml2/idp_metadata_parser.py b/src/onelogin/saml2/idp_metadata_parser.py
index 5ff705b7..281177a2 100644
--- a/src/onelogin/saml2/idp_metadata_parser.py
+++ b/src/onelogin/saml2/idp_metadata_parser.py
@@ -163,8 +163,8 @@ def parse(
if len(slo_nodes) > 0:
idp_slo_url = slo_nodes[0].get('Location', None)
- signing_nodes = OneLogin_Saml2_Utils.query(idp_descriptor_node, "./md:KeyDescriptor[not(contains(@use, 'signing'))]/ds:KeyInfo/ds:X509Data/ds:X509Certificate")
- encryption_nodes = OneLogin_Saml2_Utils.query(idp_descriptor_node, "./md:KeyDescriptor[not(contains(@use, 'encryption'))]/ds:KeyInfo/ds:X509Data/ds:X509Certificate")
+ signing_nodes = OneLogin_Saml2_Utils.query(idp_descriptor_node, "./md:KeyDescriptor[not(contains(@use, 'encryption'))]/ds:KeyInfo/ds:X509Data/ds:X509Certificate")
+ encryption_nodes = OneLogin_Saml2_Utils.query(idp_descriptor_node, "./md:KeyDescriptor[not(contains(@use, 'signing'))]/ds:KeyInfo/ds:X509Data/ds:X509Certificate")
if len(signing_nodes) > 0 or len(encryption_nodes) > 0:
certs = {}
diff --git a/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py b/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py
index 53e9b2b0..a6f53654 100644
--- a/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py
+++ b/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py
@@ -334,10 +334,10 @@ def test_parse_multi_certs(self):
"binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
},
"x509certMulti": {
- "signing": [
+ "encryption": [
"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=="
],
- "encryption": [
+ "signing": [
"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==",
"MIICZDCCAc2gAwIBAgIBADANBgkqhkiG9w0BAQ0FADBPMQswCQYDVQQGEwJ1czEUMBIGA1UECAwLZXhhbXBsZS5jb20xFDASBgNVBAoMC2V4YW1wbGUuY29tMRQwEgYDVQQDDAtleGFtcGxlLmNvbTAeFw0xNzA0MTUxNjMzMThaFw0xODA0MTUxNjMzMThaME8xCzAJBgNVBAYTAnVzMRQwEgYDVQQIDAtleGFtcGxlLmNvbTEUMBIGA1UECgwLZXhhbXBsZS5jb20xFDASBgNVBAMMC2V4YW1wbGUuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC6GLkl5lDUZdHNDAojp5i24OoPlqrt5TGXJIPqAZYT1hQvJW5nv17MFDHrjmtEnmW4ACKEy0fAX80QWIcHunZSkbEGHb+NG/6oTi5RipXMvmHnfFnPJJ0AdtiLiPE478CV856gXekV4Xx5u3KrylcOgkpYsp0GMIQBDzleMUXlYQIDAQABo1AwTjAdBgNVHQ4EFgQUnP8vlYPGPL2n6ZzDYij2kMDC8wMwHwYDVR0jBBgwFoAUnP8vlYPGPL2n6ZzDYij2kMDC8wMwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQ0FAAOBgQAlQGAl+b8Cpot1g+65lLLjVoY7APJPWLW0klKQNlMU0s4MU+71Y3ExUEOXDAZgKcFoavb1fEOGMwEf38NaJAy1e/l6VNuixXShffq20ymqHQxOG0q8ujeNkgZF9k6XDfn/QZ3AD0o/IrCT7UMc/0QsfgIjWYxwCvp2syApc5CYfQ=="
]
@@ -513,10 +513,10 @@ def testMergeSettings(self):
"binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
},
"x509certMulti": {
- "signing": [
+ "encryption": [
"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=="
],
- "encryption": [
+ "signing": [
"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==",
"MIICZDCCAc2gAwIBAgIBADANBgkqhkiG9w0BAQ0FADBPMQswCQYDVQQGEwJ1czEUMBIGA1UECAwLZXhhbXBsZS5jb20xFDASBgNVBAoMC2V4YW1wbGUuY29tMRQwEgYDVQQDDAtleGFtcGxlLmNvbTAeFw0xNzA0MTUxNjMzMThaFw0xODA0MTUxNjMzMThaME8xCzAJBgNVBAYTAnVzMRQwEgYDVQQIDAtleGFtcGxlLmNvbTEUMBIGA1UECgwLZXhhbXBsZS5jb20xFDASBgNVBAMMC2V4YW1wbGUuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC6GLkl5lDUZdHNDAojp5i24OoPlqrt5TGXJIPqAZYT1hQvJW5nv17MFDHrjmtEnmW4ACKEy0fAX80QWIcHunZSkbEGHb+NG/6oTi5RipXMvmHnfFnPJJ0AdtiLiPE478CV856gXekV4Xx5u3KrylcOgkpYsp0GMIQBDzleMUXlYQIDAQABo1AwTjAdBgNVHQ4EFgQUnP8vlYPGPL2n6ZzDYij2kMDC8wMwHwYDVR0jBBgwFoAUnP8vlYPGPL2n6ZzDYij2kMDC8wMwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQ0FAAOBgQAlQGAl+b8Cpot1g+65lLLjVoY7APJPWLW0klKQNlMU0s4MU+71Y3ExUEOXDAZgKcFoavb1fEOGMwEf38NaJAy1e/l6VNuixXShffq20ymqHQxOG0q8ujeNkgZF9k6XDfn/QZ3AD0o/IrCT7UMc/0QsfgIjWYxwCvp2syApc5CYfQ=="
]
From d5c9f7ba927d8a237ce59628970dca8937cd2406 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Mon, 15 May 2017 13:38:06 +0200
Subject: [PATCH 108/255] Minor improvement
---
src/onelogin/saml2/idp_metadata_parser.py | 148 +++++++++++-----------
1 file changed, 73 insertions(+), 75 deletions(-)
diff --git a/src/onelogin/saml2/idp_metadata_parser.py b/src/onelogin/saml2/idp_metadata_parser.py
index 281177a2..7e2bb704 100644
--- a/src/onelogin/saml2/idp_metadata_parser.py
+++ b/src/onelogin/saml2/idp_metadata_parser.py
@@ -135,84 +135,82 @@ def parse(
idp_entity_id = want_authn_requests_signed = idp_name_id_format = idp_sso_url = idp_slo_url = certs = None
if len(entity_descriptor_nodes) > 0:
- for entity_descriptor_node in entity_descriptor_nodes:
- idp_descriptor_nodes = OneLogin_Saml2_Utils.query(entity_descriptor_node, './md:IDPSSODescriptor')
- if len(idp_descriptor_nodes) > 0:
- idp_descriptor_node = idp_descriptor_nodes[0]
-
- idp_entity_id = entity_descriptor_node.get('entityID', None)
-
- want_authn_requests_signed = entity_descriptor_node.get('WantAuthnRequestsSigned', None)
-
- name_id_format_nodes = OneLogin_Saml2_Utils.query(idp_descriptor_node, './md:NameIDFormat')
- if len(name_id_format_nodes) > 0:
- idp_name_id_format = name_id_format_nodes[0].text
-
- sso_nodes = OneLogin_Saml2_Utils.query(
- idp_descriptor_node,
- "./md:SingleSignOnService[@Binding='%s']" % required_sso_binding
- )
-
- if len(sso_nodes) > 0:
- idp_sso_url = sso_nodes[0].get('Location', None)
-
- slo_nodes = OneLogin_Saml2_Utils.query(
- idp_descriptor_node,
- "./md:SingleLogoutService[@Binding='%s']" % required_slo_binding
- )
- if len(slo_nodes) > 0:
- idp_slo_url = slo_nodes[0].get('Location', None)
-
- signing_nodes = OneLogin_Saml2_Utils.query(idp_descriptor_node, "./md:KeyDescriptor[not(contains(@use, 'encryption'))]/ds:KeyInfo/ds:X509Data/ds:X509Certificate")
- encryption_nodes = OneLogin_Saml2_Utils.query(idp_descriptor_node, "./md:KeyDescriptor[not(contains(@use, 'signing'))]/ds:KeyInfo/ds:X509Data/ds:X509Certificate")
-
- if len(signing_nodes) > 0 or len(encryption_nodes) > 0:
- certs = {}
- if len(signing_nodes) > 0:
- certs['signing'] = []
- for cert_node in signing_nodes:
- certs['signing'].append(''.join(cert_node.text.split()))
- if len(encryption_nodes) > 0:
- certs['encryption'] = []
- for cert_node in encryption_nodes:
- certs['encryption'].append(''.join(cert_node.text.split()))
-
- data['idp'] = {}
-
- if idp_entity_id is not None:
- data['idp']['entityId'] = idp_entity_id
-
- if idp_sso_url is not None:
- data['idp']['singleSignOnService'] = {}
- data['idp']['singleSignOnService']['url'] = idp_sso_url
- data['idp']['singleSignOnService']['binding'] = required_sso_binding
-
- if idp_slo_url is not None:
- data['idp']['singleLogoutService'] = {}
- data['idp']['singleLogoutService']['url'] = idp_slo_url
- data['idp']['singleLogoutService']['binding'] = required_slo_binding
-
- if certs is not None:
- if len(certs) == 1 or \
- (('signing' in certs and len(certs['signing']) == 1) and
- ('encryption' in certs and len(certs['encryption']) == 1 and
- certs['signing'][0] == certs['encryption'][0])):
- if 'signing' in certs:
- data['idp']['x509cert'] = certs['signing'][0]
- else:
- data['idp']['x509cert'] = certs['encryption'][0]
+ entity_descriptor_node = entity_descriptor_nodes[0]
+ idp_descriptor_nodes = OneLogin_Saml2_Utils.query(entity_descriptor_node, './md:IDPSSODescriptor')
+ if len(idp_descriptor_nodes) > 0:
+ idp_descriptor_node = idp_descriptor_nodes[0]
+
+ idp_entity_id = entity_descriptor_node.get('entityID', None)
+
+ want_authn_requests_signed = entity_descriptor_node.get('WantAuthnRequestsSigned', None)
+
+ name_id_format_nodes = OneLogin_Saml2_Utils.query(idp_descriptor_node, './md:NameIDFormat')
+ if len(name_id_format_nodes) > 0:
+ idp_name_id_format = name_id_format_nodes[0].text
+
+ sso_nodes = OneLogin_Saml2_Utils.query(
+ idp_descriptor_node,
+ "./md:SingleSignOnService[@Binding='%s']" % required_sso_binding
+ )
+
+ if len(sso_nodes) > 0:
+ idp_sso_url = sso_nodes[0].get('Location', None)
+
+ slo_nodes = OneLogin_Saml2_Utils.query(
+ idp_descriptor_node,
+ "./md:SingleLogoutService[@Binding='%s']" % required_slo_binding
+ )
+ if len(slo_nodes) > 0:
+ idp_slo_url = slo_nodes[0].get('Location', None)
+
+ signing_nodes = OneLogin_Saml2_Utils.query(idp_descriptor_node, "./md:KeyDescriptor[not(contains(@use, 'encryption'))]/ds:KeyInfo/ds:X509Data/ds:X509Certificate")
+ encryption_nodes = OneLogin_Saml2_Utils.query(idp_descriptor_node, "./md:KeyDescriptor[not(contains(@use, 'signing'))]/ds:KeyInfo/ds:X509Data/ds:X509Certificate")
+
+ if len(signing_nodes) > 0 or len(encryption_nodes) > 0:
+ certs = {}
+ if len(signing_nodes) > 0:
+ certs['signing'] = []
+ for cert_node in signing_nodes:
+ certs['signing'].append(''.join(cert_node.text.split()))
+ if len(encryption_nodes) > 0:
+ certs['encryption'] = []
+ for cert_node in encryption_nodes:
+ certs['encryption'].append(''.join(cert_node.text.split()))
+
+ data['idp'] = {}
+
+ if idp_entity_id is not None:
+ data['idp']['entityId'] = idp_entity_id
+
+ if idp_sso_url is not None:
+ data['idp']['singleSignOnService'] = {}
+ data['idp']['singleSignOnService']['url'] = idp_sso_url
+ data['idp']['singleSignOnService']['binding'] = required_sso_binding
+
+ if idp_slo_url is not None:
+ data['idp']['singleLogoutService'] = {}
+ data['idp']['singleLogoutService']['url'] = idp_slo_url
+ data['idp']['singleLogoutService']['binding'] = required_slo_binding
+
+ if certs is not None:
+ if len(certs) == 1 or \
+ (('signing' in certs and len(certs['signing']) == 1) and
+ ('encryption' in certs and len(certs['encryption']) == 1 and
+ certs['signing'][0] == certs['encryption'][0])):
+ if 'signing' in certs:
+ data['idp']['x509cert'] = certs['signing'][0]
else:
- data['idp']['x509certMulti'] = certs
+ data['idp']['x509cert'] = certs['encryption'][0]
+ else:
+ data['idp']['x509certMulti'] = certs
- if want_authn_requests_signed is not None:
- data['security'] = {}
- data['security']['authnRequestsSigned'] = want_authn_requests_signed
+ if want_authn_requests_signed is not None:
+ data['security'] = {}
+ data['security']['authnRequestsSigned'] = want_authn_requests_signed
- if idp_name_id_format:
- data['sp'] = {}
- data['sp']['NameIDFormat'] = idp_name_id_format
-
- break
+ if idp_name_id_format:
+ data['sp'] = {}
+ data['sp']['NameIDFormat'] = idp_name_id_format
return data
@staticmethod
From 3d9245ab21477a7d1b5a6013ba7f1e6bfb295b3f Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Thu, 18 May 2017 12:27:12 +0200
Subject: [PATCH 109/255] #194 Publish KeyDescriptor[use=encryption] only when
required
---
src/onelogin/saml2/metadata.py | 21 ++++++++------
src/onelogin/saml2/settings.py | 6 ++--
.../src/OneLogin/saml2_tests/settings_test.py | 28 +++++++++++++++++--
3 files changed, 41 insertions(+), 14 deletions(-)
diff --git a/src/onelogin/saml2/metadata.py b/src/onelogin/saml2/metadata.py
index d8bc0c75..7561ab6e 100644
--- a/src/onelogin/saml2/metadata.py
+++ b/src/onelogin/saml2/metadata.py
@@ -227,7 +227,7 @@ def sign_metadata(metadata, key, cert, sign_algorithm=OneLogin_Saml2_Constants.R
return OneLogin_Saml2_Utils.add_sign(metadata, key, cert, False, sign_algorithm, digest_algorithm)
@staticmethod
- def add_x509_key_descriptors(metadata, cert=None):
+ def add_x509_key_descriptors(metadata, cert=None, add_encryption=True):
"""
Adds the x509 descriptors (sign/encryption) to the metadata
The same cert will be used for sign/encrypt
@@ -238,6 +238,9 @@ def add_x509_key_descriptors(metadata, cert=None):
:param cert: x509 cert
:type cert: string
+ :param add_encryption: Determines if the KeyDescriptor[use="encryption"] should be added.
+ :type add_encryption: boolean
+
:returns: Metadata with KeyDescriptors
:rtype: string
"""
@@ -265,18 +268,18 @@ def add_x509_key_descriptors(metadata, cert=None):
sp_sso_descriptor = entity_descriptor.getElementsByTagName('md:SPSSODescriptor')[0]
sp_sso_descriptor.insertBefore(key_descriptor.cloneNode(True), sp_sso_descriptor.firstChild)
- sp_sso_descriptor.insertBefore(key_descriptor.cloneNode(True), sp_sso_descriptor.firstChild)
+ if add_encryption:
+ sp_sso_descriptor.insertBefore(key_descriptor.cloneNode(True), sp_sso_descriptor.firstChild)
signing = xml.getElementsByTagName('md:KeyDescriptor')[0]
signing.setAttribute('use', 'signing')
-
- encryption = xml.getElementsByTagName('md:KeyDescriptor')[1]
- encryption.setAttribute('use', 'encryption')
-
signing.appendChild(key_info)
- encryption.appendChild(key_info.cloneNode(True))
-
signing.setAttribute('xmlns:ds', OneLogin_Saml2_Constants.NS_DS)
- encryption.setAttribute('xmlns:ds', OneLogin_Saml2_Constants.NS_DS)
+
+ if add_encryption:
+ encryption = xml.getElementsByTagName('md:KeyDescriptor')[1]
+ encryption.setAttribute('use', 'encryption')
+ encryption.appendChild(key_info.cloneNode(True))
+ encryption.setAttribute('xmlns:ds', OneLogin_Saml2_Constants.NS_DS)
return xml.toxml()
diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py
index 01bd5087..77a8592a 100644
--- a/src/onelogin/saml2/settings.py
+++ b/src/onelogin/saml2/settings.py
@@ -619,11 +619,13 @@ def get_sp_metadata(self):
self.get_contacts(), self.get_organization()
)
+ add_encryption = self.__security['wantNameIdEncrypted'] or self.__security['wantAssertionsEncrypted']
+
cert_new = self.get_sp_cert_new()
- metadata = OneLogin_Saml2_Metadata.add_x509_key_descriptors(metadata, cert_new)
+ metadata = OneLogin_Saml2_Metadata.add_x509_key_descriptors(metadata, cert_new, add_encryption)
cert = self.get_sp_cert()
- metadata = OneLogin_Saml2_Metadata.add_x509_key_descriptors(metadata, cert)
+ metadata = OneLogin_Saml2_Metadata.add_x509_key_descriptors(metadata, cert, add_encryption)
# Sign metadata
if 'signMetadata' in self.__security and self.__security['signMetadata'] is not False:
diff --git a/tests/src/OneLogin/saml2_tests/settings_test.py b/tests/src/OneLogin/saml2_tests/settings_test.py
index df08a6a7..ab7fd97a 100644
--- a/tests/src/OneLogin/saml2_tests/settings_test.py
+++ b/tests/src/OneLogin/saml2_tests/settings_test.py
@@ -341,7 +341,10 @@ def testGetSPMetadata(self):
Tests the getSPMetadata method of the OneLogin_Saml2_Settings
Case unsigned metadata
"""
- settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
+ settings_info = self.loadSettingsJSON()
+ settings_info['security']['wantNameIdEncrypted'] = False
+ settings_info['security']['wantAssertionsEncrypted'] = False
+ settings = OneLogin_Saml2_Settings(settings_info)
metadata = settings.get_sp_metadata()
self.assertNotEqual(len(metadata), 0)
@@ -352,20 +355,39 @@ def testGetSPMetadata(self):
self.assertIn(' ', metadata)
self.assertIn(' ', metadata)
self.assertIn('urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified ', metadata)
+ self.assertEquals(1, metadata.count('
Date: Thu, 18 May 2017 13:01:51 +0200
Subject: [PATCH 110/255] Release 2.2.2
---
changelog.md | 18 +++++++++++++++++-
setup.py | 2 +-
.../src/OneLogin/saml2_tests/settings_test.py | 2 +-
3 files changed, 19 insertions(+), 3 deletions(-)
diff --git a/changelog.md b/changelog.md
index 5346c467..7d8b3173 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,7 +1,23 @@
# python-saml changelog
+### 2.2.2 (May 18, 2017)
+* Be able to relax SSL Certificate verification when retrieving idp metadata
+* [#195](https://github.com/onelogin/python-saml/pull/195) Be able to register future SP x509cert on the settings and publish it on SP metadata
+* [#195](https://github.com/onelogin/python-saml/pull/195) Be able to register more than 1 Identity Provider x509cert, linked with an specific use (signing or encryption
+* [#195](https://github.com/onelogin/python-saml/pull/195) Allow metadata to be retrieved from source containing data of multiple entities
+* [#195](https://github.com/onelogin/python-saml/pull/195) Adapt IdP XML metadata parser to take care of multiple IdP certtificates and be able to inject the data obtained on the settings.
+* [#194](https://github.com/onelogin/python-saml/pull/194) Publish KeyDescriptor[use=encryption] only when required
+* [#190](https://github.com/onelogin/python-saml/pull/190) Checking the status of response before assertion count
+* Add Pyramid demo example
+* Allows underscores in URL hosts
+* NameID Format improvements
+* [#184](https://github.com/onelogin/python-saml/pull/184) Be able to provide a NameIDFormat to LogoutRequest
+* [#180](https://github.com/onelogin/python-saml/pull/180) Add DigestMethod support. (Add sign_algorithm and digest_algorithm parameters to sign_metadata and add_sign)
+* Validate serial number as string to work around libxml2 limitation
+* Make the Issuer on the Response Optional
+
### 2.2.1 (Jan 11, 2017)
-* [#175]((https://github.com/onelogin/python-saml/pull/175) Optionally raise detailed exceptions vs. returning False.
+* [#175](https://github.com/onelogin/python-saml/pull/175) Optionally raise detailed exceptions vs. returning False.
Implement a more specific exception class for handling some validation errors. Improve/Fix tests
* [#171](https://github.com/onelogin/python-saml/pull/171) Add hooks to retrieve last-sent and last-received requests and responses
* Improved inResponse validation on Responses
diff --git a/setup.py b/setup.py
index dfbd4b8e..37a8fbd6 100644
--- a/setup.py
+++ b/setup.py
@@ -9,7 +9,7 @@
setup(
name='python-saml',
- version='2.2.1',
+ version='2.2.2',
description='Onelogin Python Toolkit. Add SAML support to your Python software using this library',
classifiers=[
'Development Status :: 5 - Production/Stable',
diff --git a/tests/src/OneLogin/saml2_tests/settings_test.py b/tests/src/OneLogin/saml2_tests/settings_test.py
index ab7fd97a..bcdd7e1f 100644
--- a/tests/src/OneLogin/saml2_tests/settings_test.py
+++ b/tests/src/OneLogin/saml2_tests/settings_test.py
@@ -365,7 +365,7 @@ def testGetSPMetadata(self):
metadata = settings.get_sp_metadata()
self.assertEquals(2, metadata.count('
Date: Fri, 19 May 2017 09:45:48 -0400
Subject: [PATCH 111/255] https missing from request dict
If you do not have the https set in the request dict, when auth.login is called it will not build the relaystate url properly if your relaystate url should be https.
---
README.md | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 7ad159f3..6393a814 100644
--- a/README.md
+++ b/README.md
@@ -543,6 +543,7 @@ This parameter has the following scheme:
```javascript
req = {
+ "https": ""
"http_host": "",
"script_name": "",
"server_port": "",
@@ -574,7 +575,7 @@ def prepare_from_flask_request(request):
'post_data': request.form.copy()
}
```
-
+The https dictionary entry should be set to on for https requests and off for http
#### Initiate SSO ####
From d9b0ce59010eba0def48515e12e5fdd1f8b45983 Mon Sep 17 00:00:00 2001
From: piotch
Date: Wed, 31 May 2017 22:53:47 +0200
Subject: [PATCH 112/255] Upgrade to version 1.3.3 of dm.xmlsec.binding
---
setup.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/setup.py b/setup.py
index 37a8fbd6..26f73f1a 100644
--- a/setup.py
+++ b/setup.py
@@ -32,7 +32,7 @@
},
test_suite='tests',
install_requires=[
- 'dm.xmlsec.binding==1.3.2',
+ 'dm.xmlsec.binding==1.3.3',
'isodate>=0.5.0',
'defusedxml==0.4.1',
],
From 9dc6cbd116e28014ac8148f1ee9b71a09afac45b Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Wed, 31 May 2017 23:59:48 +0200
Subject: [PATCH 113/255] Fix pep8
---
src/onelogin/saml2/metadata.py | 2 +-
tests/pep8.rc | 4 ++--
tests/src/OneLogin/saml2_tests/utils_test.py | 4 ++--
3 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/src/onelogin/saml2/metadata.py b/src/onelogin/saml2/metadata.py
index 7561ab6e..c431be48 100644
--- a/src/onelogin/saml2/metadata.py
+++ b/src/onelogin/saml2/metadata.py
@@ -268,7 +268,7 @@ def add_x509_key_descriptors(metadata, cert=None, add_encryption=True):
sp_sso_descriptor = entity_descriptor.getElementsByTagName('md:SPSSODescriptor')[0]
sp_sso_descriptor.insertBefore(key_descriptor.cloneNode(True), sp_sso_descriptor.firstChild)
- if add_encryption:
+ if add_encryption:
sp_sso_descriptor.insertBefore(key_descriptor.cloneNode(True), sp_sso_descriptor.firstChild)
signing = xml.getElementsByTagName('md:KeyDescriptor')[0]
diff --git a/tests/pep8.rc b/tests/pep8.rc
index b60901e4..910e1eaa 100644
--- a/tests/pep8.rc
+++ b/tests/pep8.rc
@@ -1,3 +1,3 @@
[pep8]
-ignore = E501
-max-line-length = 160
\ No newline at end of file
+ignore = E501, E731
+max-line-length = 160
diff --git a/tests/src/OneLogin/saml2_tests/utils_test.py b/tests/src/OneLogin/saml2_tests/utils_test.py
index 459b0996..82dded99 100644
--- a/tests/src/OneLogin/saml2_tests/utils_test.py
+++ b/tests/src/OneLogin/saml2_tests/utils_test.py
@@ -655,11 +655,11 @@ def testDeleteLocalSession(self):
OneLogin_Saml2_Utils.delete_local_session()
self.assertEqual(1, local_session_test)
- dscb = lambda: self.session_cear()
+ dscb = lambda: self.session_clear()
OneLogin_Saml2_Utils.delete_local_session(dscb)
self.assertEqual(0, local_session_test)
- def session_cear(self):
+ def session_clear(self):
"""
Auxiliar method to test the delete_local_session method of the OneLogin_Saml2_Utils
"""
From 0799719409282c77672b846004cf9da2cb8de942 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Thu, 15 Jun 2017 20:00:15 +0200
Subject: [PATCH 114/255] Replaces some etree.tostring calls, that were
introduced recfently, by the sanitized call provided by defusedxml . Release
2.2.3
---
README.md | 2 ++
changelog.md | 4 ++++
setup.py | 2 +-
src/onelogin/saml2/auth.py | 4 ++--
src/onelogin/saml2/response.py | 7 +++----
src/onelogin/saml2/utils.py | 6 +++---
6 files changed, 15 insertions(+), 10 deletions(-)
diff --git a/README.md b/README.md
index 7ad159f3..cece876d 100644
--- a/README.md
+++ b/README.md
@@ -14,6 +14,8 @@ Python3: [python3-saml](https://github.com/onelogin/python3-saml).
#### Warning ####
+Update python-saml to 2.2.3, this version replaces some etree.tostring calls, that were introduced recfently, by the sanitized call provided by defusedxml
+
Update python-saml to 2.2.0, this version includes a security patch that contains extra validations that will prevent signature wrapping attacks. [CVE-2016-1000252](https://github.com/distributedweaknessfiling/DWF-Database-Artifacts/blob/master/DWF/2016/1000252/CVE-2016-1000252.json)
python-saml < v2.2.0 is vulnerable and allows signature wrapping!
diff --git a/changelog.md b/changelog.md
index 7d8b3173..a245715a 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,4 +1,8 @@
# python-saml changelog
+### 2.2.3 (Jun 15, 2017)
+* Replace some etree.tostring calls, that were introduced recfently, by the sanitized call provided by defusedxml
+* Update dm.xmlsec.binding requirement to 1.3.3 version
+
### 2.2.2 (May 18, 2017)
* Be able to relax SSL Certificate verification when retrieving idp metadata
* [#195](https://github.com/onelogin/python-saml/pull/195) Be able to register future SP x509cert on the settings and publish it on SP metadata
diff --git a/setup.py b/setup.py
index 26f73f1a..7e9b9a83 100644
--- a/setup.py
+++ b/setup.py
@@ -9,7 +9,7 @@
setup(
name='python-saml',
- version='2.2.2',
+ version='2.2.3',
description='Onelogin Python Toolkit. Add SAML support to your Python software using this library',
classifiers=[
'Development Status :: 5 - Production/Stable',
diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py
index 6e56467a..5a7f5a14 100644
--- a/src/onelogin/saml2/auth.py
+++ b/src/onelogin/saml2/auth.py
@@ -13,7 +13,7 @@
from base64 import b64encode
from urllib import quote_plus
-from lxml import etree
+from defusedxml.lxml import tostring
from onelogin.saml2.settings import OneLogin_Saml2_Settings
from onelogin.saml2.response import OneLogin_Saml2_Response
@@ -486,7 +486,7 @@ def get_last_response_xml(self, pretty_print_if_possible=False):
if isinstance(self.__last_response, basestring):
response = self.__last_response
else:
- response = etree.tostring(self.__last_response, pretty_print=pretty_print_if_possible)
+ response = tostring(self.__last_response, pretty_print=pretty_print_if_possible)
return response
def get_last_request_xml(self):
diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py
index a97ac860..4d66f590 100644
--- a/src/onelogin/saml2/response.py
+++ b/src/onelogin/saml2/response.py
@@ -11,8 +11,7 @@
from base64 import b64decode
from copy import deepcopy
-from lxml import etree
-from defusedxml.lxml import fromstring
+from defusedxml.lxml import tostring, fromstring
from xml.dom.minidom import Document
from onelogin.saml2.constants import OneLogin_Saml2_Constants
@@ -107,7 +106,7 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False):
if self.__settings.is_strict():
no_valid_xml_msg = 'Invalid SAML Response. Not match the saml-schema-protocol-2.0.xsd'
res = OneLogin_Saml2_Utils.validate_xml(
- etree.tostring(self.document),
+ tostring(self.document),
'saml-schema-protocol-2.0.xsd',
self.__settings.is_debug_active()
)
@@ -120,7 +119,7 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False):
# If encrypted, check also the decrypted document
if self.encrypted:
res = OneLogin_Saml2_Utils.validate_xml(
- etree.tostring(self.decrypted_document),
+ tostring(self.decrypted_document),
'saml-schema-protocol-2.0.xsd',
self.__settings.is_debug_active()
)
diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py
index 826c8a87..3d19835c 100644
--- a/src/onelogin/saml2/utils.py
+++ b/src/onelogin/saml2/utils.py
@@ -153,7 +153,7 @@ def validate_xml(xml, schema, debug=False):
return 'invalid_xml'
- return parseString(etree.tostring(dom, encoding='unicode').encode('utf-8'))
+ return parseString(tostring(dom, encoding='unicode').encode('utf-8'))
@staticmethod
def format_cert(cert, heads=True):
@@ -680,7 +680,7 @@ def generate_name_id(value, sp_nq, sp_format, cert=None, debug=False, nq=None):
edata = enc_ctx.encryptXml(enc_data, elem[0])
- newdoc = parseString(etree.tostring(edata, encoding='unicode').encode('utf-8'))
+ newdoc = parseString(tostring(edata, encoding='unicode').encode('utf-8'))
if newdoc.hasChildNodes():
child = newdoc.firstChild
@@ -897,7 +897,7 @@ def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constant
dsig_ctx.signKey = sign_key
dsig_ctx.sign(signature)
- newdoc = parseString(etree.tostring(elem, encoding='unicode').encode('utf-8'))
+ newdoc = parseString(tostring(elem, encoding='unicode').encode('utf-8'))
signature_nodes = newdoc.getElementsByTagName("Signature")
From 36cd9c5d303bb10d8176914e14e07dbb9196fd4d Mon Sep 17 00:00:00 2001
From: Florent PIGOUT
Date: Fri, 16 Jun 2017 11:43:57 +0200
Subject: [PATCH 115/255] Fix typo in README
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index cece876d..dd340b24 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@ Python3: [python3-saml](https://github.com/onelogin/python3-saml).
#### Warning ####
-Update python-saml to 2.2.3, this version replaces some etree.tostring calls, that were introduced recfently, by the sanitized call provided by defusedxml
+Update python-saml to 2.2.3, this version replaces some etree.tostring calls, that were introduced recently, by the sanitized call provided by defusedxml
Update python-saml to 2.2.0, this version includes a security patch that contains extra validations that will prevent signature wrapping attacks. [CVE-2016-1000252](https://github.com/distributedweaknessfiling/DWF-Database-Artifacts/blob/master/DWF/2016/1000252/CVE-2016-1000252.json)
From fac4767038591e046994aa9335fefda58368bde4 Mon Sep 17 00:00:00 2001
From: Florent PIGOUT
Date: Fri, 23 Jun 2017 11:52:14 +0200
Subject: [PATCH 116/255] Get NameID when element decrypted twice
---
changelog.md | 3 ++
src/onelogin/saml2/response.py | 3 +-
src/onelogin/saml2/utils.py | 8 +++-
tests/data/misc/sp4.key | 28 ++++++++++++
tests/src/OneLogin/saml2_tests/utils_test.py | 47 ++++++++++++++++++--
5 files changed, 84 insertions(+), 5 deletions(-)
create mode 100644 tests/data/misc/sp4.key
diff --git a/changelog.md b/changelog.md
index a245715a..bb3f142b 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,4 +1,7 @@
# python-saml changelog
+### 2.2.4 (unreleased)
+* Get NameID when element decrypted twice
+
### 2.2.3 (Jun 15, 2017)
* Replace some etree.tostring calls, that were introduced recfently, by the sanitized call provided by defusedxml
* Update dm.xmlsec.binding requirement to 1.3.3 version
diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py
index 4d66f590..eb5f73a4 100644
--- a/src/onelogin/saml2/response.py
+++ b/src/onelogin/saml2/response.py
@@ -798,8 +798,9 @@ def __decrypt_assertion(self, dom):
keyinfo.append(encrypted_key[0])
encrypted_data = encrypted_data_nodes[0]
- decrypted = OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key, debug)
+ decrypted = OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key, debug=debug, inplace=True)
dom.replace(encrypted_assertion_nodes[0], decrypted)
+
return dom
def get_error(self):
diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py
index 3d19835c..19f1511a 100644
--- a/src/onelogin/saml2/utils.py
+++ b/src/onelogin/saml2/utils.py
@@ -10,6 +10,7 @@
"""
import base64
+from copy import deepcopy
from datetime import datetime
import calendar
from hashlib import sha1, sha256, sha384, sha512
@@ -746,7 +747,7 @@ def get_status(dom):
return status
@staticmethod
- def decrypt_element(encrypted_data, key, debug=False):
+ def decrypt_element(encrypted_data, key, debug=False, inplace=False):
"""
Decrypts an encrypted element.
@@ -759,6 +760,9 @@ def decrypt_element(encrypted_data, key, debug=False):
:param debug: Activate the xmlsec debug
:type: bool
+ :param inplace: update passed data with decrypted result
+ :type: bool
+
:returns: The decrypted element.
:rtype: lxml.etree.Element
"""
@@ -766,6 +770,8 @@ def decrypt_element(encrypted_data, key, debug=False):
encrypted_data = fromstring(str(encrypted_data.toxml()))
elif isinstance(encrypted_data, basestring):
encrypted_data = fromstring(str(encrypted_data))
+ elif not inplace and isinstance(encrypted_data, etree._Element):
+ encrypted_data = deepcopy(encrypted_data)
error_callback_method = None
if debug:
diff --git a/tests/data/misc/sp4.key b/tests/data/misc/sp4.key
new file mode 100644
index 00000000..8be7be60
--- /dev/null
+++ b/tests/data/misc/sp4.key
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQD4ZrcXcjCBOQS7
+stUabuXPYnXKvcoJUrMVPRX1zfrXvpfghCrykbL1TKoqGfmEA9oNRoMBOmZCgLlK
+eb0TfuEO/u1jf4rRFcK7U/dYEiX74bQgUnJUWTfFlhwPjxGhn9zDrc2tSpworJBV
+amyBZIo5Beap5OJLote/Wqp1DZjNyEZ2m8m+lv8udmejmlo5RMoIzuG3VdH6ADC9
+LKF+QsXC/HRZBhLE/y+75/XrNODvX8eM8+9Xp21QlVF1EIZDfNQ2iHsA8GEpJDC5
+aomTW/xExBysejnwP2ROrfm3PIfP64EbB4G01f8eErlXeUD0oQ0gECgIXsJpfBkD
+IWMHwx3/AgMBAAECggEAdbLNvFlJ7GDlAj75RJ4ZXAuOPrNw4LwDyON53U9tNP7F
+HgfiBa/NuPdLhclq9geRMUsg1dsjCw3NPiGy2mL7JszaFJQhZXLHI1Xk1CE9SD0o
+yUvniln/2CqJP0IOG6QQydM3qo24snkZpq9XnHPUHrLSGdwu8aHGUpAWRoJbzdzR
+tBWBn6SlkuaE52vcGh7eMdKSICRCg2/gg6LIi89pkiI9tfozAL2LPcDTRGp3DA3w
+U6OO8k+d1La4s9G0i22OGSwPxGerTHnBIzpeM/ivRwBypFy3EV9bbjQlheI53xAo
+ZMmGeSnQ89MWgY64pnWrX862Mf1EZYTjumDe2dl1kQKBgQD9pBG2BbcQ8qieTf84
+92LeOYTPRdd0N+gdyDKKorRO772zgxBwpSwO285nzy/FKSnpJIDtuee6OFClnDor
+Ui6lG1WPQeoSEdH1V10XkfSaoFOz7Hyv9H2dCLvW/VO9KYq07VAmQcvNZnqIW+tI
+edSHcQ3I8tnw4CiFa0BPvdhk9wKBgQD6tiuN2NvuNFFLvwpBGp3hjGyn6siyXDyP
+8IXQmP66NxKqcX/NafVO3bVh6VrPGd7PL1PloQZ5EBG2PPtRdf/g4aeZKZleCUXm
+9OgMEOUqdbTP9TGrmgNPtNBx3jnhnX/GTy/7GK77YlXEVplezWaerwRM7NCFCtp2
+W6K1M961OQKBgQDDSznr2hirrvuP8GRMW4a/rrAI3DDZplZN4CCySDbm9IcvGgJl
+iXgT9MDHg2q3t0sy3U18PYEkDEpkSZcsVfneXN6TEGCHCzuLWXovNM2O5VWtmrAi
+1vCFIf1nuuRoKP1I89SbsFuYyogcSBIwWsX+h1ji2cJfSmlI2VzKSVW93wKBgQDA
+sqwfRoMkP0oM8jUrfQ3Egm4xUiAYFxTlfXUcs7t13UaXgs08USifCYGUVAvcCoJa
+tIHDiVS0UEmMzKpOHmghrM9oxbR/tpjnv21reMDrNbVX8ZnPz3ykEtHz816BrtC6
+17qFQJ+d0CMj2XvghfdOGC8yAQL0fzcSqbQRmmCe4QKBgFWY9fqHEKdG/UlxZfBB
+C/QRNTJsrbZf9Ok/o1h6BHnK64xUc4elShEwV9IdC4QNW0UCr7WXoGLUkhfUphId
+q//KUDNc7VrWj5URsZcGi7WMkqNm9kPkpeuh3iSvh3+q7tK0/yfuj9ZQOjKzQnit
+VZBooJAJGdSqYgitpyxB71/n
+-----END PRIVATE KEY-----
diff --git a/tests/src/OneLogin/saml2_tests/utils_test.py b/tests/src/OneLogin/saml2_tests/utils_test.py
index 82dded99..cbc36d66 100644
--- a/tests/src/OneLogin/saml2_tests/utils_test.py
+++ b/tests/src/OneLogin/saml2_tests/utils_test.py
@@ -5,6 +5,7 @@
from base64 import b64decode
import json
+from defusedxml.lxml import fromstring
from lxml import etree
from os.path import dirname, join, exists
import unittest
@@ -718,15 +719,28 @@ def testDecryptElement(self):
key2 = f.read()
f.close()
- with self.assertRaisesRegexp(Exception, "('failed to decrypt', -1)"):
- OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key2)
+ # sp.key and sp2.key are equivalent we should be able to decrypt the nameID again
+ decrypted_nameid = OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key2)
+ self.assertIn('{%s}NameID' % (OneLogin_Saml2_Constants.NS_SAML), decrypted_nameid.tag)
+ self.assertEqual('457bdb600de717891c77647b0806ce59c089d5b8', decrypted_nameid.text)
key_3_file_name = join(self.data_path, 'misc', 'sp3.key')
f = open(key_3_file_name, 'r')
key3 = f.read()
f.close()
+
+ # sp.key and sp3.key are equivalent we should be able to decrypt the nameID again
+ decrypted_nameid = OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key3)
+ self.assertIn('{%s}NameID' % (OneLogin_Saml2_Constants.NS_SAML), decrypted_nameid.tag)
+ self.assertEqual('457bdb600de717891c77647b0806ce59c089d5b8', decrypted_nameid.text)
+
+ key_4_file_name = join(self.data_path, 'misc', 'sp4.key')
+ f = open(key_4_file_name, 'r')
+ key4 = f.read()
+ f.close()
+
with self.assertRaisesRegexp(Exception, "('failed to decrypt', -1)"):
- OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key3)
+ OneLogin_Saml2_Utils.decrypt_element(encrypted_data, key4)
xml_nameid_enc_2 = b64decode(self.file_contents(join(self.data_path, 'responses', 'invalids', 'encrypted_nameID_without_EncMethod.xml.base64')))
dom_nameid_enc_2 = parseString(xml_nameid_enc_2)
@@ -744,6 +758,33 @@ def testDecryptElement(self):
with self.assertRaisesRegexp(Exception, "('failed to decrypt', -1)"):
OneLogin_Saml2_Utils.decrypt_element(encrypted_data_3, key)
+ def testDecryptElementInplace(self):
+ """
+ Tests the decrypt_element method of the OneLogin_Saml2_Utils with inplace=True
+ """
+ settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
+
+ key = settings.get_sp_key()
+
+ xml_nameid_enc = b64decode(self.file_contents(join(self.data_path, 'responses', 'response_encrypted_nameid.xml.base64')))
+ dom = fromstring(xml_nameid_enc)
+ encrypted_node = dom.xpath('//saml:EncryptedID/xenc:EncryptedData', namespaces=OneLogin_Saml2_Constants.NSMAP)[0]
+
+ # can be decrypted twice when copy the node first
+ for _ in range(2):
+ decrypted_nameid = OneLogin_Saml2_Utils.decrypt_element(encrypted_node, key, inplace=False)
+ self.assertIn('NameID', decrypted_nameid.tag)
+ self.assertEqual('2de11defd199f8d5bb63f9b7deb265ba5c675c10', decrypted_nameid.text)
+
+ # can only be decrypted once in place
+ decrypted_nameid = OneLogin_Saml2_Utils.decrypt_element(encrypted_node, key, inplace=True)
+ self.assertIn('NameID', decrypted_nameid.tag)
+ self.assertEqual('2de11defd199f8d5bb63f9b7deb265ba5c675c10', decrypted_nameid.text)
+
+ # can't be decrypted twice since it has been dcrypted inplace
+ with self.assertRaisesRegexp(Exception, "('failed to decrypt', -1)"):
+ OneLogin_Saml2_Utils.decrypt_element(encrypted_node, key, inplace=True)
+
def testAddSign(self):
"""
Tests the add_sign method of the OneLogin_Saml2_Utils
From 295364013442d3460f03e7bc832bb94c50e3fbc9 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Wed, 26 Jul 2017 13:00:26 +0200
Subject: [PATCH 117/255] Fix minor typo
---
README.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index ea9291a5..9bb01b16 100644
--- a/README.md
+++ b/README.md
@@ -865,7 +865,7 @@ else:
### SP Key rollover ###
-If you plan to update the SP x509cert and privateKey you can define the new x509cert as $settings['sp']['x509certNew'] 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,7 +874,7 @@ 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']['x509certMulti'] parameter.
+In order to handle that the toolkit offers the settings['idp']['x509certMulti'] parameter.
When that parameter is used, 'x509cert' and 'certFingerprint' values will be ignored by the toolkit.
From b5ff71417a2fde394a214087d9e404392ee52a0d Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Wed, 13 Sep 2017 12:29:29 +0200
Subject: [PATCH 118/255] Fix #204. On a LogoutRequest if the NameIdFormat is
entity, NameQualifier and SPNameQualifier will be ommited. If the
NameIdFormat is not entity and a NameQualifier is provided, then the
SPNameQualifier will be also added. Update info related to LogoutRequest on
the README
---
README.md | 8 ++++++-
src/onelogin/saml2/logout_request.py | 9 ++++++--
tests/src/OneLogin/saml2_tests/auth_test.py | 2 +-
.../saml2_tests/logout_request_test.py | 22 ++++++++++++++++++-
4 files changed, 36 insertions(+), 5 deletions(-)
diff --git a/README.md b/README.md
index 9bb01b16..2e6842ee 100644
--- a/README.md
+++ b/README.md
@@ -801,11 +801,17 @@ target_url = 'https://example.com'
auth.logout(return_to=target_url)
```
-Also there are 2 optional parameters that can be set:
+Also there are 4 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
+
+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
diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py
index 99c97198..6e6a1903 100644
--- a/src/onelogin/saml2/logout_request.py
+++ b/src/onelogin/saml2/logout_request.py
@@ -80,10 +80,15 @@ def __init__(self, settings, request=None, name_id=None, session_index=None, nq=
nameIdFormat = name_id_format
else:
nameIdFormat = sp_data['NameIDFormat']
- spNameQualifier = None
else:
- name_id = idp_data['entityId']
nameIdFormat = OneLogin_Saml2_Constants.NAMEID_ENTITY
+
+ spNameQualifier = None
+ if nameIdFormat == OneLogin_Saml2_Constants.NAMEID_ENTITY:
+ name_id = idp_data['entityId']
+ nq = None
+ elif nq is not None:
+ # We only gonna include SPNameQualifier if NameQualifier is provided
spNameQualifier = sp_data['entityId']
name_id_obj = OneLogin_Saml2_Utils.generate_name_id(
diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py
index 9c42c0bc..f80d26e1 100644
--- a/tests/src/OneLogin/saml2_tests/auth_test.py
+++ b/tests/src/OneLogin/saml2_tests/auth_test.py
@@ -1047,7 +1047,7 @@ def testGetLastLogoutRequest(self):
expectedFragment = (
' Destination="http://idp.example.com/SingleLogoutService.php">\n'
' http://stuff.com/endpoints/metadata.php \n'
- ' http://idp.example.com/ \n'
+ ' http://idp.example.com/ \n'
' \n '
)
self.assertIn(expectedFragment, auth.get_last_request_xml())
diff --git a/tests/src/OneLogin/saml2_tests/logout_request_test.py b/tests/src/OneLogin/saml2_tests/logout_request_test.py
index b0bfdf92..d1eab57e 100644
--- a/tests/src/OneLogin/saml2_tests/logout_request_test.py
+++ b/tests/src/OneLogin/saml2_tests/logout_request_test.py
@@ -154,9 +154,11 @@ def testGetNameIdData(self):
OneLogin_Saml2_Logout_Request.get_nameid_data(dom_2.toxml(), key)
idp_data = settings.get_idp_data()
+ sp_data = settings.get_sp_data()
expected_name_id_data = {
'Format': 'urn:oasis:names:tc:SAML:2.0:nameid-format:emailAddress',
'NameQualifier': idp_data['entityId'],
+ 'SPNameQualifier': sp_data['entityId'],
'Value': 'ONELOGIN_9c86c4542ab9d6fce07f2f7fd335287b9b3cdf69'
}
@@ -169,6 +171,24 @@ def testGetNameIdData(self):
name_id_data_3 = OneLogin_Saml2_Logout_Request.get_nameid_data(dom)
self.assertEqual(expected_name_id_data, name_id_data_3)
+ expected_name_id_data = {
+ 'Format': 'urn:oasis:names:tc:SAML:2.0:nameid-format:emailAddress',
+ 'Value': 'ONELOGIN_9c86c4542ab9d6fce07f2f7fd335287b9b3cdf69'
+ }
+ logout_request = OneLogin_Saml2_Logout_Request(settings, None, expected_name_id_data['Value'], None, None, expected_name_id_data['Format'])
+ dom = parseString(logout_request.get_xml())
+ name_id_data_4 = OneLogin_Saml2_Logout_Request.get_nameid_data(dom)
+ self.assertEqual(expected_name_id_data, name_id_data_4)
+
+ expected_name_id_data = {
+ 'Format': 'urn:oasis:names:tc:SAML:2.0:nameid-format:entity',
+ 'Value': 'http://idp.example.com/'
+ }
+ logout_request = OneLogin_Saml2_Logout_Request(settings)
+ dom = parseString(logout_request.get_xml())
+ name_id_data_5 = OneLogin_Saml2_Logout_Request.get_nameid_data(dom)
+ self.assertEqual(expected_name_id_data, name_id_data_5)
+
def testGetNameId(self):
"""
Tests the get_nameid of the OneLogin_Saml2_LogoutRequest
@@ -478,7 +498,7 @@ def testGetXML(self):
expectedFragment = (
'Destination="http://idp.example.com/SingleLogoutService.php">\n'
' http://stuff.com/endpoints/metadata.php \n'
- ' http://idp.example.com/ \n'
+ ' http://idp.example.com/ \n'
' \n '
)
self.assertIn(expectedFragment, logout_request_generated.get_xml())
From e70d0cd8f3c772ebd0b4606fd7ce02b5d3cce97d Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Thu, 14 Sep 2017 15:33:21 +0200
Subject: [PATCH 119/255] Be able to get at the auth object the last processed
ID (response/assertion) and the last generated ID. Reset errorReason
attribute of the auth object after each Process method
---
README.md | 13 +++
src/onelogin/saml2/auth.py | 36 +++++++-
src/onelogin/saml2/logout_response.py | 2 +
src/onelogin/saml2/response.py | 30 +++++++
.../data/responses/valid_response.xml.base64 | 2 +-
tests/src/OneLogin/saml2_tests/auth_test.py | 82 ++++++++++++++++++-
.../src/OneLogin/saml2_tests/response_test.py | 49 +++++++++++
7 files changed, 210 insertions(+), 4 deletions(-)
diff --git a/README.md b/README.md
index 2e6842ee..aa740fce 100644
--- a/README.md
+++ b/README.md
@@ -889,6 +889,13 @@ The 'x509certMulti' is an array with 2 keys:
- 'encryption' An array with one unique cert that will be used to encrypt data to be sent to the IdP
+### Replay attacks ###
+
+ In order to avoid reply attacks, you can store the ID of the SAML messages already processed, to avoid processing them twice. Since the Messages expires and will be invalidated due that fact, you don't need to store those IDs longer than the time frame that you currently accepting.
+
+ Get the ID of the last processed message/assertion with the get_last_message_id/get_last_assertion_id method of the Auth object.
+
+
### Main classes and methods ###
Described below are the main classes and methods that can be invoked from the SAML2 library.
@@ -920,6 +927,9 @@ Main class of OneLogin Python Toolkit
* ***set_strict*** Set the strict mode active/disable.
* ***get_last_request_xml*** Returns the most recently-constructed/processed XML SAML request (AuthNRequest, LogoutRequest)
* ***get_last_response_xml*** Returns the most recently-constructed/processed XML SAML response (SAMLResponse, LogoutResponse). If the SAMLResponse had an encrypted assertion, decrypts it.
+* ***get_last_message_id*** The ID of the last Response SAML message processed.
+* ***get_last_assertion_id*** The ID of the last assertion processed.
+* ***get_last_assertion_not_on_or_after*** The NotOnOrAfter value of the valid SubjectConfirmationData node (if any) of the last assertion processed (is only calculated with strict = true)
#### OneLogin_Saml2_Auth - authn_request.py ####
@@ -948,6 +958,9 @@ SAML 2 Authentication Response class
* ***validate_timestamps*** Verifies that the document is valid according to Conditions Element
* ***get_error*** After execute a validation process, if fails this method returns the cause
* ***get_xml_document*** Returns the SAML Response document (If contains an encrypted assertion, decrypts it).
+* ***get_id*** the ID of the response
+* ***get_assertion_id*** the ID of the assertion in the response
+* ***get_assertion_not_on_or_after*** the NotOnOrAfter value of the valid SubjectConfirmationData if any
#### OneLogin_Saml2_LogoutRequest - logout_request.py ####
diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py
index 5a7f5a14..bb564e4d 100644
--- a/src/onelogin/saml2/auth.py
+++ b/src/onelogin/saml2/auth.py
@@ -59,6 +59,9 @@ def __init__(self, request_data, old_settings=None, custom_base_path=None):
self.__errors = []
self.__error_reason = None
self.__last_request_id = None
+ self.__last_message_id = None
+ self.__last_assertion_id = None
+ self.__last_assertion_not_on_or_after = None
self.__last_request = None
self.__last_response = None
@@ -90,6 +93,7 @@ def process_response(self, request_id=None):
:raises: OneLogin_Saml2_Error.SAML_RESPONSE_NOT_FOUND, when a POST with a SAMLResponse is not found
"""
self.__errors = []
+ self.__error_reason = None
if 'post_data' in self.__request_data and 'SAMLResponse' in self.__request_data['post_data']:
# AuthnResponse -- HTTP_POST Binding
@@ -101,6 +105,9 @@ def process_response(self, request_id=None):
self.__nameid_format = response.get_nameid_format()
self.__session_index = response.get_session_index()
self.__session_expiration = response.get_session_not_on_or_after()
+ self.__last_message_id = response.get_id()
+ self.__last_assertion_id = response.get_assertion_id()
+ self.__last_assertion_not_on_or_after = response.get_assertion_not_on_or_after()
self.__authenticated = True
else:
@@ -127,6 +134,7 @@ def process_slo(self, keep_local_session=False, request_id=None, delete_session_
:returns: Redirection URL
"""
self.__errors = []
+ self.__error_reason = None
if 'get_data' in self.__request_data and 'SAMLResponse' in self.__request_data['get_data']:
logout_response = OneLogin_Saml2_Logout_Response(self.__settings, self.__request_data['get_data']['SAMLResponse'])
@@ -136,8 +144,10 @@ def process_slo(self, keep_local_session=False, request_id=None, delete_session_
self.__error_reason = logout_response.get_error()
elif logout_response.get_status() != OneLogin_Saml2_Constants.STATUS_SUCCESS:
self.__errors.append('logout_not_success')
- elif not keep_local_session:
- OneLogin_Saml2_Utils.delete_local_session(delete_session_cb)
+ else:
+ self.__last_message_id = logout_response.id
+ if not keep_local_session:
+ OneLogin_Saml2_Utils.delete_local_session(delete_session_cb)
elif 'get_data' in self.__request_data and 'SAMLRequest' in self.__request_data['get_data']:
logout_request = OneLogin_Saml2_Logout_Request(self.__settings, self.__request_data['get_data']['SAMLRequest'])
@@ -150,6 +160,7 @@ def process_slo(self, keep_local_session=False, request_id=None, delete_session_
OneLogin_Saml2_Utils.delete_local_session(delete_session_cb)
in_response_to = logout_request.id
+ self.__last_message_id = logout_request.id
response_builder = OneLogin_Saml2_Logout_Response(self.__settings)
response_builder.build(in_response_to)
self.__last_response = response_builder.get_xml()
@@ -241,6 +252,13 @@ def get_session_expiration(self):
"""
return self.__session_expiration
+ def get_last_assertion_not_on_or_after(self):
+ """
+ The NotOnOrAfter value of the valid SubjectConfirmationData node
+ (if any) of the last assertion processed
+ """
+ return self.__last_assertion_not_on_or_after
+
def get_errors(self):
"""
Returns a list with code errors if something went wrong
@@ -282,6 +300,20 @@ def get_last_request_id(self):
"""
return self.__last_request_id
+ def get_last_message_id(self):
+ """
+ :returns: The ID of the last Response SAML message processed.
+ :rtype: string
+ """
+ return self.__last_message_id
+
+ def get_last_assertion_id(self):
+ """
+ :returns: The ID of the last assertion processed.
+ :rtype: string
+ """
+ return self.__last_assertion_id
+
def login(self, return_to=None, force_authn=False, is_passive=False, set_nameid_policy=True):
"""
Initiates the SSO process.
diff --git a/src/onelogin/saml2/logout_response.py b/src/onelogin/saml2/logout_response.py
index f37a3585..eec5cc94 100644
--- a/src/onelogin/saml2/logout_response.py
+++ b/src/onelogin/saml2/logout_response.py
@@ -40,10 +40,12 @@ def __init__(self, settings, response=None):
"""
self.__settings = settings
self.__error = None
+ self.id = None
if response is not None:
self.__logout_response = OneLogin_Saml2_Utils.decode_base64_and_inflate(response)
self.document = parseString(self.__logout_response)
+ self.id = self.document.documentElement.getAttribute('ID')
def get_issuer(self):
"""
diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py
index eb5f73a4..d57f8c42 100644
--- a/src/onelogin/saml2/response.py
+++ b/src/onelogin/saml2/response.py
@@ -43,6 +43,7 @@ def __init__(self, settings, response):
self.document = fromstring(self.response)
self.decrypted_document = None
self.encrypted = None
+ self.valid_scd_not_on_or_after = None
# Quick check for the presence of EncryptedAssertion
encrypted_assertion_nodes = self.__query('/samlp:Response/saml:EncryptedAssertion')
@@ -258,6 +259,10 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False):
parsed_nb = OneLogin_Saml2_Utils.parse_SAML_to_time(nb)
if parsed_nb > OneLogin_Saml2_Utils.now():
continue
+
+ if nooa:
+ self.valid_scd_not_on_or_after = OneLogin_Saml2_Utils.parse_SAML_to_time(nooa)
+
any_subject_confirmation = True
break
@@ -487,6 +492,12 @@ def get_session_not_on_or_after(self):
not_on_or_after = OneLogin_Saml2_Utils.parse_SAML_to_time(authn_statement_nodes[0].get('SessionNotOnOrAfter'))
return not_on_or_after
+ def get_assertion_not_on_or_after(self):
+ """
+ Returns the NotOnOrAfter value of the valid SubjectConfirmationData node if any
+ """
+ return self.valid_scd_not_on_or_after
+
def get_session_index(self):
"""
Gets the SessionIndex from the AuthnStatement
@@ -820,3 +831,22 @@ def get_xml_document(self):
return self.decrypted_document
else:
return self.document
+
+ def get_id(self):
+ """
+ :returns: the ID of the response
+ :rtype: string
+ """
+ return self.document.get('ID', None)
+
+ def get_assertion_id(self):
+ """
+ :returns: the ID of the assertion in the response
+ :rtype: string
+ """
+ if not self.validate_num_assertions():
+ raise OneLogin_Saml2_ValidationError(
+ 'SAML Response must contain 1 assertion',
+ OneLogin_Saml2_ValidationError.WRONG_NUMBER_OF_ASSERTIONS
+ )
+ return self.__query_assertion('')[0].get('ID', None)
diff --git a/tests/data/responses/valid_response.xml.base64 b/tests/data/responses/valid_response.xml.base64
index 42ff8eb4..5a917f2f 100644
--- a/tests/data/responses/valid_response.xml.base64
+++ b/tests/data/responses/valid_response.xml.base64
@@ -1 +1 @@
-PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeGNjMzE1NjhiLWM0NmQtZmY3NS1iYTJlLTUzMDM0ODQ5ODBkYSIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciPjxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+DQogICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPg0KICA8ZHM6UmVmZXJlbmNlIFVSST0iI3BmeGNjMzE1NjhiLWM0NmQtZmY3NS1iYTJlLTUzMDM0ODQ5ODBkYSI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz48ZHM6RGlnZXN0VmFsdWU+cTZnVllaZUJmV21TelhzUUNEVUFYc2hyNjVRPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT5ES041ZWVwbm5CU3NvNVd3VHFwZ1lxQ2hsUXA1YXlVYUUrWlVVZ09CK2t5RDJEOUMxRjlSblVTK1VTa2hJQ2dDWVNTamtqQWE4OTZNNzgzdFFMd3dwcGZBSG1NMWpUcTRUVm1xKzlQOTZyQ29LUzUwR0FiZHVOUXFlSTl2T1EraTlXRWVkcFFFeWFsNEJNbS9pTkxHNE00Lzl5alRrVTU2OUdOOGZBOUJSb1E9PC9kczpTaWduYXR1cmVWYWx1ZT4NCjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDA4YzJhNmJiLTdlZTQtOGRjMi04ZmUyLWYwNTVlZWQ5M2RlNCIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIj48c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPg0KICA8ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPg0KICAgIDxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjcnNhLXNoYTEiLz4NCiAgPGRzOlJlZmVyZW5jZSBVUkk9IiNwZngwOGMyYTZiYi03ZWU0LThkYzItOGZlMi1mMDU1ZWVkOTNkZTQiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPkVnQzhKSU1zL2VHaXdiSTBVc3AvWGRYUEtnOD08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+MW9JZFJ1bUJDaUNOdXVtYzVYNkhQYWI1L2xXZjZqR0dOR0dGOWxKRWxOc2lOYThpK3dSdkEveVF1YUFvMmp5bGNnV1pMWmZscjc2VnYrYVF0ZFA0LzNDR0djall5cUxWc0k0SS9iZjdXakk0dW1nMXl6aU5zNzMrelUzQThKK21LV013Q1J0eEZibm9BcnNyZzVFcVdOT2MrYkdWMEFsYnFCZTF4RllGcGVnPTwvZHM6U2lnbmF0dXJlVmFsdWU+DQo8ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE+PGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlDZ1RDQ0Flb0NDUUNiT2xyV0RkWDdGVEFOQmdrcWhraUc5dzBCQVFVRkFEQ0JoREVMTUFrR0ExVUVCaE1DVGs4eEdEQVdCZ05WQkFnVEQwRnVaSEpsWVhNZ1UyOXNZbVZ5WnpFTU1Bb0dBMVVFQnhNRFJtOXZNUkF3RGdZRFZRUUtFd2RWVGtsT1JWUlVNUmd3RmdZRFZRUURFdzltWldsa1pTNWxjbXhoYm1jdWJtOHhJVEFmQmdrcWhraUc5dzBCQ1FFV0VtRnVaSEpsWVhOQWRXNXBibVYwZEM1dWJ6QWVGdzB3TnpBMk1UVXhNakF4TXpWYUZ3MHdOekE0TVRReE1qQXhNelZhTUlHRU1Rc3dDUVlEVlFRR0V3Sk9UekVZTUJZR0ExVUVDQk1QUVc1a2NtVmhjeUJUYjJ4aVpYSm5NUXd3Q2dZRFZRUUhFd05HYjI4eEVEQU9CZ05WQkFvVEIxVk9TVTVGVkZReEdEQVdCZ05WQkFNVEQyWmxhV1JsTG1WeWJHRnVaeTV1YnpFaE1COEdDU3FHU0liM0RRRUpBUllTWVc1a2NtVmhjMEIxYm1sdVpYUjBMbTV2TUlHZk1BMEdDU3FHU0liM0RRRUJBUVVBQTRHTkFEQ0JpUUtCZ1FEaXZiaFI3UDUxNngvUzNCcUt4dXBRZTBMT05vbGl1cGlCT2VzQ08zU0hiRHJsMytxOUliZm5mbUUwNHJOdU1jUHNJeEIxNjFUZERwSWVzTENuN2M4YVBISVNLT3RQbEFlVFpTbmI4UUF1N2FSalpxMytQYnJQNXVXM1RjZkNHUHRLVHl0SE9nZS9PbEpibzA3OGRWaFhRMTRkMUVEd1hKVzFyUlh1VXQ0QzhRSURBUUFCTUEwR0NTcUdTSWIzRFFFQkJRVUFBNEdCQUNEVmZwODZIT2JxWStlOEJVb1dROStWTVF4MUFTRG9oQmp3T3NnMld5a1VxUlhGK2RMZmNVSDlkV1I2M0N0WklLRkRiU3ROb21QblF6N25iSytvbnlnd0JzcFZFYm5IdVVpaFpxM1pVZG11bVFxQ3c0VXZzLzFVdnEzb3JPby9XSlZoVHl2TGdGVksyUWFyUTQvNjdPWmZIZDdSK1BPQlhob3BoU012MVpPbzwvZHM6WDUwOUNlcnRpZmljYXRlPjwvZHM6WDUwOURhdGE+PC9kczpLZXlJbmZvPjwvZHM6U2lnbmF0dXJlPjxzYW1sOlN1YmplY3Q+PHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvbWV0YWRhdGEucGhwIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+NDkyODgyNjE1YWNmMzFjODA5NmI2MjcyNDVkNzZhZTUzMDM2YzA5MDwvc2FtbDpOYW1lSUQ+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAyMy0wOC0yM1QwNjo1NzowMVoiIFJlY2lwaWVudD0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciLz48L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj48L3NhbWw6U3ViamVjdD48c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxNC0wMi0xOVQwMTozNjozMVoiIE5vdE9uT3JBZnRlcj0iMjAyMy0wOC0yM1QwNjo1NzowMVoiPjxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PHNhbWw6QXVkaWVuY2U+aHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvbWV0YWRhdGEucGhwPC9zYW1sOkF1ZGllbmNlPjwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPjwvc2FtbDpDb25kaXRpb25zPjxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxNC0wMi0xOVQwMTozNzowMVoiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwMTQtMDItMTlUMDk6Mzc6MDFaIiBTZXNzaW9uSW5kZXg9Il82MjczZDc3YjhjZGUwYzMzM2VjNzlkMjJhOWZhMDAwM2I5ZmUyZDc1Y2IiPjxzYW1sOkF1dGhuQ29udGV4dD48c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj48L3NhbWw6QXV0aG5Db250ZXh0Pjwvc2FtbDpBdXRoblN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlIE5hbWU9InVpZCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+c21hcnRpbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5zbWFydGluQHlhY28uZXM8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0iY24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPlNpeHRvMzwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJzbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+TWFydGluMjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJlZHVQZXJzb25BZmZpbGlhdGlvbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dXNlcjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5hZG1pbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PC9zYW1sOkFzc2VydGlvbj48L3NhbWxwOlJlc3BvbnNlPg==
\ No newline at end of file
+PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeDQyYmU0MGJmLTM5YzMtNzdmMC1jNmFlLThiZjJlMjNhMWEyZSIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciPjxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+DQogICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPg0KICA8ZHM6UmVmZXJlbmNlIFVSST0iI3BmeDQyYmU0MGJmLTM5YzMtNzdmMC1jNmFlLThiZjJlMjNhMWEyZSI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz48ZHM6RGlnZXN0VmFsdWU+M1JNaTI0V0F2cjlnTHdWZ0NtUDlsM2NneCtFPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT5FR2ZSVnRTblJqVkJwdkpMdjExNnhWcmovaDVVQnJlTDNnV0V3ZnNORHkrMU9iaC9XTHZlR01uS2xIN0draHU5eXNIUVkxYzRnSER4SVgyaXZtM3YzVFhqK0V3ZDVhMVE2dXgvbXZJSFRvSUR5SnFLL25LSUtVZFEwTWhIcHVXUnF1OEJoWGZQcnRGdWkzOHRhamJpNjFTUnMxN0ZJc3YvbFJLb1JScWJIYlU9PC9kczpTaWduYXR1cmVWYWx1ZT4NCjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDU3ZGZkYTYwLWIyMTEtNGNkYS0wZjYzLTZkNWRlYjY5ZTViYiIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIj48c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPg0KICA8ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPg0KICAgIDxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjcnNhLXNoYTEiLz4NCiAgPGRzOlJlZmVyZW5jZSBVUkk9IiNwZng1N2RmZGE2MC1iMjExLTRjZGEtMGY2My02ZDVkZWI2OWU1YmIiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPndIVUpDalpLRWVtd3E2eGZzMkNIbUd3UXNIND08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+Y2VFd0NtbFQ2d3lpdGVLZE5JaVNFLzBoTG5rMkRweEh0K24vdzlhTHp4MmpneDJOTzBiUTFjb0xyYlBmc1A1SjhNYkNBQWRUb20yUkxaTUxIdTNwZjBHeXJ6cUUxTkhIMmthaGJiSWtKYnZJWkhhaE1JaFZNUW1RMzhJMDdQRC8wc3BjdkJqQS9lblk0SWtWR2VQdmUwV3FhaU5ZUGNOeDNaTDJPdENMZEtzPTwvZHM6U2lnbmF0dXJlVmFsdWU+DQo8ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE+PGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlDZ1RDQ0Flb0NDUUNiT2xyV0RkWDdGVEFOQmdrcWhraUc5dzBCQVFVRkFEQ0JoREVMTUFrR0ExVUVCaE1DVGs4eEdEQVdCZ05WQkFnVEQwRnVaSEpsWVhNZ1UyOXNZbVZ5WnpFTU1Bb0dBMVVFQnhNRFJtOXZNUkF3RGdZRFZRUUtFd2RWVGtsT1JWUlVNUmd3RmdZRFZRUURFdzltWldsa1pTNWxjbXhoYm1jdWJtOHhJVEFmQmdrcWhraUc5dzBCQ1FFV0VtRnVaSEpsWVhOQWRXNXBibVYwZEM1dWJ6QWVGdzB3TnpBMk1UVXhNakF4TXpWYUZ3MHdOekE0TVRReE1qQXhNelZhTUlHRU1Rc3dDUVlEVlFRR0V3Sk9UekVZTUJZR0ExVUVDQk1QUVc1a2NtVmhjeUJUYjJ4aVpYSm5NUXd3Q2dZRFZRUUhFd05HYjI4eEVEQU9CZ05WQkFvVEIxVk9TVTVGVkZReEdEQVdCZ05WQkFNVEQyWmxhV1JsTG1WeWJHRnVaeTV1YnpFaE1COEdDU3FHU0liM0RRRUpBUllTWVc1a2NtVmhjMEIxYm1sdVpYUjBMbTV2TUlHZk1BMEdDU3FHU0liM0RRRUJBUVVBQTRHTkFEQ0JpUUtCZ1FEaXZiaFI3UDUxNngvUzNCcUt4dXBRZTBMT05vbGl1cGlCT2VzQ08zU0hiRHJsMytxOUliZm5mbUUwNHJOdU1jUHNJeEIxNjFUZERwSWVzTENuN2M4YVBISVNLT3RQbEFlVFpTbmI4UUF1N2FSalpxMytQYnJQNXVXM1RjZkNHUHRLVHl0SE9nZS9PbEpibzA3OGRWaFhRMTRkMUVEd1hKVzFyUlh1VXQ0QzhRSURBUUFCTUEwR0NTcUdTSWIzRFFFQkJRVUFBNEdCQUNEVmZwODZIT2JxWStlOEJVb1dROStWTVF4MUFTRG9oQmp3T3NnMld5a1VxUlhGK2RMZmNVSDlkV1I2M0N0WklLRkRiU3ROb21QblF6N25iSytvbnlnd0JzcFZFYm5IdVVpaFpxM1pVZG11bVFxQ3c0VXZzLzFVdnEzb3JPby9XSlZoVHl2TGdGVksyUWFyUTQvNjdPWmZIZDdSK1BPQlhob3BoU012MVpPbzwvZHM6WDUwOUNlcnRpZmljYXRlPjwvZHM6WDUwOURhdGE+PC9kczpLZXlJbmZvPjwvZHM6U2lnbmF0dXJlPjxzYW1sOlN1YmplY3Q+PHNhbWw6TmFtZUlEIFNQTmFtZVF1YWxpZmllcj0iaHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvbWV0YWRhdGEucGhwIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+NDkyODgyNjE1YWNmMzFjODA5NmI2MjcyNDVkNzZhZTUzMDM2YzA5MDwvc2FtbDpOYW1lSUQ+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjA1NC0wOC0yM1QwNjo1NzowMVoiIFJlY2lwaWVudD0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciLz48L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj48L3NhbWw6U3ViamVjdD48c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxNC0wMi0xOVQwMTozNjozMVoiIE5vdE9uT3JBZnRlcj0iMjA1NC0wOC0yM1QwNjo1NzowMVoiPjxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PHNhbWw6QXVkaWVuY2U+aHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvbWV0YWRhdGEucGhwPC9zYW1sOkF1ZGllbmNlPjwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPjwvc2FtbDpDb25kaXRpb25zPjxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxNC0wMi0xOVQwMTozNzowMVoiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwNTQtMDItMTlUMDk6Mzc6MDFaIiBTZXNzaW9uSW5kZXg9Il82MjczZDc3YjhjZGUwYzMzM2VjNzlkMjJhOWZhMDAwM2I5ZmUyZDc1Y2IiPjxzYW1sOkF1dGhuQ29udGV4dD48c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj48L3NhbWw6QXV0aG5Db250ZXh0Pjwvc2FtbDpBdXRoblN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlIE5hbWU9InVpZCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+c21hcnRpbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5zbWFydGluQHlhY28uZXM8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0iY24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPlNpeHRvMzwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJzbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+TWFydGluMjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJlZHVQZXJzb25BZmZpbGlhdGlvbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dXNlcjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5hZG1pbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PC9zYW1sOkFzc2VydGlvbj48L3NhbWxwOlJlc3BvbnNlPg==
\ No newline at end of file
diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py
index f80d26e1..c50fc6e8 100644
--- a/tests/src/OneLogin/saml2_tests/auth_test.py
+++ b/tests/src/OneLogin/saml2_tests/auth_test.py
@@ -118,7 +118,7 @@ def testGetSessionExpiration(self):
self.assertIsNone(auth2.get_session_expiration())
auth2.process_response()
- self.assertEqual(1392802621, auth2.get_session_expiration())
+ self.assertEqual(2655106621, auth2.get_session_expiration())
def testGetLastErrorReason(self):
"""
@@ -1002,6 +1002,21 @@ def testBuildResponseSignature(self):
with self.assertRaisesRegexp(OneLogin_Saml2_Error, "Trying to sign the SAMLResponse but can't load the SP private key"):
auth2.build_response_signature(message, relay_state)
+ def testGetLastRequestID(self):
+ settings_info = self.loadSettingsJSON()
+ request_data = self.get_request()
+ auth = OneLogin_Saml2_Auth(request_data, old_settings=settings_info)
+
+ auth.login()
+ id1 = auth.get_last_request_id()
+ self.assertNotEqual(id1, None)
+
+ auth.logout()
+ id2 = auth.get_last_request_id()
+ self.assertNotEqual(id2, None)
+
+ self.assertNotEqual(id1, id2)
+
def testGetLastSAMLResponse(self):
settings = self.loadSettingsJSON()
message = self.file_contents(join(self.data_path, 'responses', 'signed_message_response.xml.base64'))
@@ -1084,6 +1099,71 @@ def testGetLastLogoutResponse(self):
auth.process_slo()
self.assertEqual(response, auth.get_last_response_xml())
+ def testGetInfoFromLastResponseReceived(self):
+ """
+ Tests the get_last_message_id, get_last_assertion_id and get_last_assertion_not_on_or_after
+ of the OneLogin_Saml2_Auth class
+ """
+ settings = self.loadSettingsJSON()
+ request_data = self.get_request()
+ message = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64'))
+ del request_data['get_data']
+ request_data['post_data'] = {
+ 'SAMLResponse': message
+ }
+ auth = OneLogin_Saml2_Auth(request_data, old_settings=settings)
+
+ auth.process_response()
+ self.assertEqual(auth.get_last_message_id(), 'pfx42be40bf-39c3-77f0-c6ae-8bf2e23a1a2e')
+ self.assertEqual(auth.get_last_assertion_id(), 'pfx57dfda60-b211-4cda-0f63-6d5deb69e5bb')
+ self.assertIsNone(auth.get_last_assertion_not_on_or_after())
+
+ # NotOnOrAfter is only calculated with strict = true
+ # If invalid, response id and assertion id are not obtained
+
+ settings['strict'] = True
+ auth = OneLogin_Saml2_Auth(request_data, old_settings=settings)
+ auth.process_response()
+ self.assertNotEqual(len(auth.get_errors()), 0)
+ self.assertIsNone(auth.get_last_message_id())
+ self.assertIsNone(auth.get_last_assertion_id())
+ self.assertIsNone(auth.get_last_assertion_not_on_or_after())
+
+ request_data['https'] = 'on'
+ request_data['http_host'] = 'pitbulk.no-ip.org'
+ request_data['script_name'] = '/newonelogin/demo1/index.php?acs'
+ auth = OneLogin_Saml2_Auth(request_data, old_settings=settings)
+ auth.process_response()
+ self.assertEqual(len(auth.get_errors()), 0)
+ self.assertEqual(auth.get_last_message_id(), 'pfx42be40bf-39c3-77f0-c6ae-8bf2e23a1a2e')
+ self.assertEqual(auth.get_last_assertion_id(), 'pfx57dfda60-b211-4cda-0f63-6d5deb69e5bb')
+ self.assertEqual(auth.get_last_assertion_not_on_or_after(), 2671081021)
+
+ def testGetIdFromLogoutRequest(self):
+ """
+ Tests the get_last_message_id of the OneLogin_Saml2_Auth class
+ Case Valid Logout request
+ """
+ settings = self.loadSettingsJSON()
+ request = self.file_contents(join(self.data_path, 'logout_requests', 'logout_request.xml'))
+ message = OneLogin_Saml2_Utils.deflate_and_base64_encode(request)
+ message_wrapper = {'get_data': {'SAMLRequest': message}}
+ auth = OneLogin_Saml2_Auth(message_wrapper, old_settings=settings)
+ auth.process_slo()
+ self.assertIn(auth.get_last_message_id(), 'ONELOGIN_21584ccdfaca36a145ae990442dcd96bfe60151e')
+
+ def testGetIdFromLogoutResponse(self):
+ """
+ Tests the get_last_message_id of the OneLogin_Saml2_Auth class
+ Case Valid Logout response
+ """
+ settings = self.loadSettingsJSON()
+ response = self.file_contents(join(self.data_path, 'logout_responses', 'logout_response.xml'))
+ message = OneLogin_Saml2_Utils.deflate_and_base64_encode(response)
+ message_wrapper = {'get_data': {'SAMLResponse': message}}
+ auth = OneLogin_Saml2_Auth(message_wrapper, old_settings=settings)
+ auth.process_slo()
+ self.assertIn(auth.get_last_message_id(), '_f9ee61bd9dbf63606faa9ae3b10548d5b3656fb859')
if __name__ == '__main__':
if is_running_under_teamcity():
diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py
index 5d6bebf2..f8c396e0 100644
--- a/tests/src/OneLogin/saml2_tests/response_test.py
+++ b/tests/src/OneLogin/saml2_tests/response_test.py
@@ -1408,6 +1408,55 @@ def testStatusCheckBeforeAssertionCheck(self):
with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'The status code of the Response was not Success, was Responder'):
response.is_valid(self.get_request_data(), raise_exceptions=True)
+ def testGetId(self):
+ """
+ Tests that we can retrieve the ID of the Response
+ """
+ settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
+ xml = self.file_contents(join(self.data_path, 'responses', 'signed_message_response.xml.base64'))
+ response = OneLogin_Saml2_Response(settings, xml)
+ self.assertEqual(response.get_id(), 'pfxc3d2b542-0f7e-8767-8e87-5b0dc6913375')
+
+ def testGetAssertionId(self):
+ """
+ Tests that we can retrieve the ID of the Assertion
+ """
+ settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
+ xml = self.file_contents(join(self.data_path, 'responses', 'signed_message_response.xml.base64'))
+ response = OneLogin_Saml2_Response(settings, xml)
+ self.assertEqual(response.get_assertion_id(), '_cccd6024116641fe48e0ae2c51220d02755f96c98d')
+
+ def testGetAssertionNotOnOrAfter(self):
+ """
+ Tests that we can retrieve the NotOnOrAfter value of
+ the valid SubjectConfirmationData
+ """
+ settings_data = self.loadSettingsJSON()
+ request_data = self.get_request_data()
+ settings = OneLogin_Saml2_Settings(settings_data)
+ message = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64'))
+ response = OneLogin_Saml2_Response(settings, message)
+ self.assertIsNone(response.get_assertion_not_on_or_after())
+
+ response.is_valid(request_data)
+ self.assertIsNone(response.get_error())
+ self.assertIsNone(response.get_assertion_not_on_or_after())
+
+ settings_data['strict'] = True
+ settings = OneLogin_Saml2_Settings(settings_data)
+ response = OneLogin_Saml2_Response(settings, message)
+
+ response.is_valid(request_data)
+ self.assertNotEqual(response.get_error(), None)
+ self.assertIsNone(response.get_assertion_not_on_or_after())
+
+ request_data['https'] = 'on'
+ request_data['http_host'] = 'pitbulk.no-ip.org'
+ request_data['script_name'] = '/newonelogin/demo1/index.php?acs'
+ response.is_valid(request_data)
+ self.assertIsNone(response.get_error())
+ self.assertEqual(response.get_assertion_not_on_or_after(), 2671081021)
+
if __name__ == '__main__':
if is_running_under_teamcity():
From df3db495c2576c706900fbd3ede2488ba0edfb4c Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Thu, 14 Sep 2017 18:08:24 +0200
Subject: [PATCH 120/255] Fix issue on getting multiple certs when only sign or
encryption certs
---
src/onelogin/saml2/idp_metadata_parser.py | 4 +-
.../idp_metadata_multi_signing_certs.xml | 75 +++++++++++++++++++
.../saml2_tests/idp_metadata_parser_test.py | 36 +++++++++
3 files changed, 114 insertions(+), 1 deletion(-)
create mode 100644 tests/data/metadata/idp_metadata_multi_signing_certs.xml
diff --git a/src/onelogin/saml2/idp_metadata_parser.py b/src/onelogin/saml2/idp_metadata_parser.py
index 7e2bb704..097b3342 100644
--- a/src/onelogin/saml2/idp_metadata_parser.py
+++ b/src/onelogin/saml2/idp_metadata_parser.py
@@ -193,7 +193,9 @@ def parse(
data['idp']['singleLogoutService']['binding'] = required_slo_binding
if certs is not None:
- if len(certs) == 1 or \
+ if (len(certs) == 1 and
+ (('signing' in certs and len(certs['signing']) == 1) or
+ ('encryption' in certs and len(certs['encryption']) == 1))) or \
(('signing' in certs and len(certs['signing']) == 1) and
('encryption' in certs and len(certs['encryption']) == 1 and
certs['signing'][0] == certs['encryption'][0])):
diff --git a/tests/data/metadata/idp_metadata_multi_signing_certs.xml b/tests/data/metadata/idp_metadata_multi_signing_certs.xml
new file mode 100644
index 00000000..0cba257a
--- /dev/null
+++ b/tests/data/metadata/idp_metadata_multi_signing_certs.xml
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+ MIIEZTCCA02gAwIBAgIUPyy/A3bZAZ4m28PzEUUoT7RJhxIwDQYJKoZIhvcNAQEF
+BQAwcjELMAkGA1UEBhMCVVMxKzApBgNVBAoMIk9uZUxvZ2luIFRlc3QgKHNnYXJj
+aWEtdXMtcHJlcHJvZCkxFTATBgNVBAsMDE9uZUxvZ2luIElkUDEfMB0GA1UEAwwW
+T25lTG9naW4gQWNjb3VudCA4OTE0NjAeFw0xNjA4MDQyMjI5MzdaFw0yMTA4MDUy
+MjI5MzdaMHIxCzAJBgNVBAYTAlVTMSswKQYDVQQKDCJPbmVMb2dpbiBUZXN0IChz
+Z2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxHzAdBgNV
+BAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDYwggEiMA0GCSqGSIb3DQEBAQUAA4IB
+DwAwggEKAoIBAQDN6iqQGcLOCglNO42I2rkzE05UXSiMXT6c8ALThMMiaDw6qqzo
+3sd/tKK+NcNKWLIIC8TozWVyh5ykUiVZps+08xil7VsTU7E+wKu3kvmOsvw2wlRw
+tnoKZJwYhnr+RkBa+h1r3ZYUgXm1ZPeHMKj1g18KaWz9+MxYL6BhKqrOzfW/P2xx
+VRcFH7/pq+ZsDdgNzD2GD+apzY4MZyZj/N6BpBWJ0GlFsmtBegpbX3LBitJuFkk5
+L4/U/jjF1AJa3boBdCUVfATqO5G03H4XS1GySjBIRQXmlUF52rLjg6xCgWJ30/+t
+1X+IHLJeixiQ0vxyh6C4/usCEt94cgD1r8ADAgMBAAGjgfIwge8wDAYDVR0TAQH/
+BAIwADAdBgNVHQ4EFgQUPW0DcH0G3IwynWgi74co4wZ6n7gwga8GA1UdIwSBpzCB
+pIAUPW0DcH0G3IwynWgi74co4wZ6n7ihdqR0MHIxCzAJBgNVBAYTAlVTMSswKQYD
+VQQKDCJPbmVMb2dpbiBUZXN0IChzZ2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQL
+DAxPbmVMb2dpbiBJZFAxHzAdBgNVBAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDaC
+FD8svwN22QGeJtvD8xFFKE+0SYcSMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0B
+AQUFAAOCAQEAQhB4q9jrycwbHrDSoYR1X4LFFzvJ9Us75wQquRHXpdyS9D6HUBXM
+GI6ahPicXCQrfLgN8vzMIiqZqfySXXv/8/dxe/X4UsWLYKYJHDJmxXD5EmWTa65c
+hjkeP1oJAc8f3CKCpcP2lOBTthbnk2fEVAeLHR4xNdQO0VvGXWO9BliYPpkYqUIB
+vlm+Fg9mF7AM/Uagq2503XXIE1Lq//HON68P10vNMwLSKOtYLsoTiCnuIKGJqG37
+MsZVjQ1ZPRcO+LSLkq0i91gFxrOrVCrgztX4JQi5XkvEsYZGIXXjwHqxTVyt3adZ
+WQO0LPxPqRiUqUzyhDhLo/xXNrHCu4VbMw==
+
+
+
+
+
+
+ MIICZDCCAc2gAwIBAgIBADANBgkqhkiG9w0BAQ0FADBPMQswCQYDVQQGEwJ1czEUMBIGA1UECAwLZXhhbXBsZS5jb20xFDASBgNVBAoMC2V4YW1wbGUuY29tMRQwEgYDVQQDDAtleGFtcGxlLmNvbTAeFw0xNzA0MTUxNjMzMThaFw0xODA0MTUxNjMzMThaME8xCzAJBgNVBAYTAnVzMRQwEgYDVQQIDAtleGFtcGxlLmNvbTEUMBIGA1UECgwLZXhhbXBsZS5jb20xFDASBgNVBAMMC2V4YW1wbGUuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC6GLkl5lDUZdHNDAojp5i24OoPlqrt5TGXJIPqAZYT1hQvJW5nv17MFDHrjmtEnmW4ACKEy0fAX80QWIcHunZSkbEGHb+NG/6oTi5RipXMvmHnfFnPJJ0AdtiLiPE478CV856gXekV4Xx5u3KrylcOgkpYsp0GMIQBDzleMUXlYQIDAQABo1AwTjAdBgNVHQ4EFgQUnP8vlYPGPL2n6ZzDYij2kMDC8wMwHwYDVR0jBBgwFoAUnP8vlYPGPL2n6ZzDYij2kMDC8wMwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQ0FAAOBgQAlQGAl+b8Cpot1g+65lLLjVoY7APJPWLW0klKQNlMU0s4MU+71Y3ExUEOXDAZgKcFoavb1fEOGMwEf38NaJAy1e/l6VNuixXShffq20ymqHQxOG0q8ujeNkgZF9k6XDfn/QZ3AD0o/IrCT7UMc/0QsfgIjWYxwCvp2syApc5CYfQ==
+
+
+
+
+
+
+ MIIEZTCCA02gAwIBAgIUPyy/A3bZAZ4m28PzEUUoT7RJhxIwDQYJKoZIhvcNAQEF
+BQAwcjELMAkGA1UEBhMCVVMxKzApBgNVBAoMIk9uZUxvZ2luIFRlc3QgKHNnYXJj
+aWEtdXMtcHJlcHJvZCkxFTATBgNVBAsMDE9uZUxvZ2luIElkUDEfMB0GA1UEAwwW
+T25lTG9naW4gQWNjb3VudCA4OTE0NjAeFw0xNjA4MDQyMjI5MzdaFw0yMTA4MDUy
+MjI5MzdaMHIxCzAJBgNVBAYTAlVTMSswKQYDVQQKDCJPbmVMb2dpbiBUZXN0IChz
+Z2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxHzAdBgNV
+BAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDYwggEiMA0GCSqGSIb3DQEBAQUAA4IB
+DwAwggEKAoIBAQDN6iqQGcLOCglNO42I2rkzE05UXSiMXT6c8ALThMMiaDw6qqzo
+3sd/tKK+NcNKWLIIC8TozWVyh5ykUiVZps+08xil7VsTU7E+wKu3kvmOsvw2wlRw
+tnoKZJwYhnr+RkBa+h1r3ZYUgXm1ZPeHMKj1g18KaWz9+MxYL6BhKqrOzfW/P2xx
+VRcFH7/pq+ZsDdgNzD2GD+apzY4MZyZj/N6BpBWJ0GlFsmtBegpbX3LBitJuFkk5
+L4/U/jjF1AJa3boBdCUVfATqO5G03H4XS1GySjBIRQXmlUF52rLjg6xCgWJ30/+t
+1X+IHLJeixiQ0vxyh6C4/usCEt94cgD1r8ADAgMBAAGjgfIwge8wDAYDVR0TAQH/
+BAIwADAdBgNVHQ4EFgQUPW0DcH0G3IwynWgi74co4wZ6n7gwga8GA1UdIwSBpzCB
+pIAUPW0DcH0G3IwynWgi74co4wZ6n7ihdqR0MHIxCzAJBgNVBAYTAlVTMSswKQYD
+VQQKDCJPbmVMb2dpbiBUZXN0IChzZ2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQL
+DAxPbmVMb2dpbiBJZFAxHzAdBgNVBAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDaC
+FD8svwN22QGeJtvD8xFFKE+0SYcSMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0B
+AQUFAAOCAQEAQhB4q9jrycwbHrDSoYR1X4LFFzvJ9Us75wQquRHXpdyS9D6HUBXM
+GI6ahPicXCQrfLgN8vzMIiqZqfySXXv/8/dxe/X4UsWLYKYJHDJmxXD5EmWTa65c
+hjkeP1oJAc8f3CKCpcP2lOBTthbnk2fEVAeLHR4xNdQO0VvGXWO9BliYPpkYqUIB
+vlm+Fg9mF7AM/Uagq2503XXIE1Lq//HON68P10vNMwLSKOtYLsoTiCnuIKGJqG37
+MsZVjQ1ZPRcO+LSLkq0i91gFxrOrVCrgztX4JQi5XkvEsYZGIXXjwHqxTVyt3adZ
+WQO0LPxPqRiUqUzyhDhLo/xXNrHCu4VbMw==
+
+
+
+
+ urn:oasis:names:tc:SAML:2.0:nameid-format:transient
+
+
+
diff --git a/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py b/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py
index a6f53654..58e2852e 100644
--- a/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py
+++ b/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py
@@ -353,6 +353,42 @@ def test_parse_multi_certs(self):
expected_settings = json.loads(expected_settings_json)
self.assertEqual(expected_settings, data)
+ def test_parse_multi_singing_certs(self):
+ """
+ Tests the parse method of the OneLogin_Saml2_IdPMetadataParser
+ Case: IdP metadata contains multiple signing certs and no encryption certs
+ """
+ xml_idp_metadata = self.file_contents(join(self.data_path, 'metadata', 'idp_metadata_multi_signing_certs.xml'))
+ data = OneLogin_Saml2_IdPMetadataParser.parse(xml_idp_metadata)
+
+ expected_settings_json = """
+ {
+ "sp": {
+ "NameIDFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:transient"
+ },
+ "idp": {
+ "singleLogoutService": {
+ "url": "https://idp.examle.com/saml/slo",
+ "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
+ },
+ "x509certMulti": {
+ "signing": [
+ "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==",
+ "MIICZDCCAc2gAwIBAgIBADANBgkqhkiG9w0BAQ0FADBPMQswCQYDVQQGEwJ1czEUMBIGA1UECAwLZXhhbXBsZS5jb20xFDASBgNVBAoMC2V4YW1wbGUuY29tMRQwEgYDVQQDDAtleGFtcGxlLmNvbTAeFw0xNzA0MTUxNjMzMThaFw0xODA0MTUxNjMzMThaME8xCzAJBgNVBAYTAnVzMRQwEgYDVQQIDAtleGFtcGxlLmNvbTEUMBIGA1UECgwLZXhhbXBsZS5jb20xFDASBgNVBAMMC2V4YW1wbGUuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC6GLkl5lDUZdHNDAojp5i24OoPlqrt5TGXJIPqAZYT1hQvJW5nv17MFDHrjmtEnmW4ACKEy0fAX80QWIcHunZSkbEGHb+NG/6oTi5RipXMvmHnfFnPJJ0AdtiLiPE478CV856gXekV4Xx5u3KrylcOgkpYsp0GMIQBDzleMUXlYQIDAQABo1AwTjAdBgNVHQ4EFgQUnP8vlYPGPL2n6ZzDYij2kMDC8wMwHwYDVR0jBBgwFoAUnP8vlYPGPL2n6ZzDYij2kMDC8wMwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQ0FAAOBgQAlQGAl+b8Cpot1g+65lLLjVoY7APJPWLW0klKQNlMU0s4MU+71Y3ExUEOXDAZgKcFoavb1fEOGMwEf38NaJAy1e/l6VNuixXShffq20ymqHQxOG0q8ujeNkgZF9k6XDfn/QZ3AD0o/IrCT7UMc/0QsfgIjWYxwCvp2syApc5CYfQ==",
+ "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=="
+ ]
+ },
+ "entityId": "https://idp.examle.com/saml/metadata",
+ "singleSignOnService": {
+ "url": "https://idp.examle.com/saml/sso",
+ "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
+ }
+ }
+ }
+ """
+ expected_settings = json.loads(expected_settings_json)
+ self.assertEqual(expected_settings, data)
+
def testMergeSettings(self):
"""
Tests the merge_settings method of the OneLogin_Saml2_IdPMetadataParser
From 30cbe7c056f9e2a3892b3c61f79ac9086aa0631e Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Thu, 14 Sep 2017 18:38:22 +0200
Subject: [PATCH 121/255] Allow empty nameid if setting wantNameId is false.
Only raise Exceptions when strict mode is enabled
---
src/onelogin/saml2/response.py | 7 ++--
.../src/OneLogin/saml2_tests/response_test.py | 32 +++++++++++++++++--
2 files changed, 34 insertions(+), 5 deletions(-)
diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py
index d57f8c42..a5992617 100644
--- a/src/onelogin/saml2/response.py
+++ b/src/onelogin/saml2/response.py
@@ -421,16 +421,19 @@ def get_nameid_data(self):
nameid_nodes = self.__query_assertion('/saml:Subject/saml:NameID')
if nameid_nodes:
nameid = nameid_nodes[0]
+
+ is_strict = self.__settings.is_strict()
+ want_nameid = self.__settings.get_security_data().get('wantNameId', True)
if nameid is None:
security = self.__settings.get_security_data()
- if security.get('wantNameId', True):
+ if is_strict and want_nameid:
raise OneLogin_Saml2_ValidationError(
'NameID not found in the assertion of the Response',
OneLogin_Saml2_ValidationError.NO_NAMEID
)
else:
- if self.__settings.is_strict() and not nameid.text:
+ if is_strict and want_nameid and not nameid.text:
raise OneLogin_Saml2_ValidationError(
'An empty NameID value found',
OneLogin_Saml2_ValidationError.EMPTY_NAMEID
diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py
index f8c396e0..e811b003 100644
--- a/tests/src/OneLogin/saml2_tests/response_test.py
+++ b/tests/src/OneLogin/saml2_tests/response_test.py
@@ -82,6 +82,7 @@ def testReturnNameId(self):
Tests the get_nameid method of the OneLogin_Saml2_Response
"""
json_settings = self.loadSettingsJSON()
+ json_settings['strict'] = True
settings = OneLogin_Saml2_Settings(json_settings)
xml = self.file_contents(join(self.data_path, 'responses', 'response1.xml.base64'))
@@ -135,11 +136,18 @@ def testReturnNameId(self):
with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'An empty NameID value found'):
response_9.get_nameid()
+ json_settings['security']['wantNameId'] = False
+ settings = OneLogin_Saml2_Settings(json_settings)
+
+ nameid_9 = response_9.get_nameid()
+ self.assertEqual(None, nameid_9)
+
def testReturnNameIdFormat(self):
"""
Tests the get_nameid_format method of the OneLogin_Saml2_Response
"""
json_settings = self.loadSettingsJSON()
+ json_settings['strict'] = True
settings = OneLogin_Saml2_Settings(json_settings)
xml = self.file_contents(join(self.data_path, 'responses', 'response1.xml.base64'))
@@ -193,11 +201,18 @@ def testReturnNameIdFormat(self):
with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'An empty NameID value found'):
response_9.get_nameid_format()
+ json_settings['security']['wantNameId'] = False
+ settings = OneLogin_Saml2_Settings(json_settings)
+
+ nameid_9 = response_9.get_nameid_format()
+ self.assertEqual('urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress', nameid_9)
+
def testGetNameIdData(self):
"""
Tests the get_nameid_data method of the OneLogin_Saml2_Response
"""
json_settings = self.loadSettingsJSON()
+ json_settings['strict'] = True
settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
xml = self.file_contents(join(self.data_path, 'responses', 'response1.xml.base64'))
@@ -231,8 +246,9 @@ def testGetNameIdData(self):
xml_4 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_nameid.xml.base64'))
response_4 = OneLogin_Saml2_Response(settings, xml_4)
- with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'NameID not found in the assertion of the Response'):
- response_4.get_nameid_data()
+
+ nameid_data_4 = response_4.get_nameid_data()
+ self.assertEqual({}, nameid_data_4)
json_settings['security']['wantNameId'] = True
settings = OneLogin_Saml2_Settings(json_settings)
@@ -262,13 +278,23 @@ def testGetNameIdData(self):
response_8 = OneLogin_Saml2_Response(settings, xml_5)
with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'The SPNameQualifier value mistmatch the SP entityID value.'):
response_8.get_nameid_data()
- self.assertTrue(False)
xml_6 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'empty_nameid.xml.base64'))
response_9 = OneLogin_Saml2_Response(settings, xml_6)
with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'An empty NameID value found'):
response_9.get_nameid_data()
+ json_settings['security']['wantNameId'] = False
+ settings = OneLogin_Saml2_Settings(json_settings)
+
+ nameid_data_9 = response_9.get_nameid_data()
+
+ expected_nameid_data_4 = {
+ 'Value': None,
+ 'Format': 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
+ }
+ self.assertEqual(expected_nameid_data_4, nameid_data_9)
+
def testCheckStatus(self):
"""
Tests the check_status method of the OneLogin_Saml2_Response
From ef91db1fb7c5b80555fe8365908a28e3838d5d4e Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Fri, 15 Sep 2017 12:39:09 +0200
Subject: [PATCH 122/255] Improve previous commited tests
---
src/onelogin/saml2/response.py | 4 +-
.../src/OneLogin/saml2_tests/response_test.py | 269 +++++++++++++-----
2 files changed, 202 insertions(+), 71 deletions(-)
diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py
index a5992617..869a4e7a 100644
--- a/src/onelogin/saml2/response.py
+++ b/src/onelogin/saml2/response.py
@@ -425,8 +425,6 @@ def get_nameid_data(self):
is_strict = self.__settings.is_strict()
want_nameid = self.__settings.get_security_data().get('wantNameId', True)
if nameid is None:
- security = self.__settings.get_security_data()
-
if is_strict and want_nameid:
raise OneLogin_Saml2_ValidationError(
'NameID not found in the assertion of the Response',
@@ -443,7 +441,7 @@ def get_nameid_data(self):
for attr in ['Format', 'SPNameQualifier', 'NameQualifier']:
value = nameid.get(attr, None)
if value:
- if self.__settings.is_strict() and attr == 'SPNameQualifier':
+ if is_strict and attr == 'SPNameQualifier':
sp_data = self.__settings.get_sp_data()
sp_entity_id = sp_data.get('entityId', '')
if sp_entity_id != value:
diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py
index e811b003..6e4c1df8 100644
--- a/tests/src/OneLogin/saml2_tests/response_test.py
+++ b/tests/src/OneLogin/saml2_tests/response_test.py
@@ -82,8 +82,7 @@ def testReturnNameId(self):
Tests the get_nameid method of the OneLogin_Saml2_Response
"""
json_settings = self.loadSettingsJSON()
- json_settings['strict'] = True
-
+ json_settings['strict'] = False
settings = OneLogin_Saml2_Settings(json_settings)
xml = self.file_contents(join(self.data_path, 'responses', 'response1.xml.base64'))
response = OneLogin_Saml2_Response(settings, xml)
@@ -97,58 +96,98 @@ def testReturnNameId(self):
response_3 = OneLogin_Saml2_Response(settings, xml_3)
self.assertEqual('_68392312d490db6d355555cfbbd8ec95d746516f60', response_3.get_nameid())
+ json_settings['strict'] = True
+ json_settings['security']['wantNameId'] = True
+ settings = OneLogin_Saml2_Settings(json_settings)
+
xml_4 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_nameid.xml.base64'))
response_4 = OneLogin_Saml2_Response(settings, xml_4)
- with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'NameID not found in the assertion of the Response'):
+ with self.assertRaisesRegexp(Exception, 'NameID not found in the assertion of the Response'):
response_4.get_nameid()
- json_settings['security']['wantNameId'] = True
+ json_settings['security']['wantNameId'] = False
settings = OneLogin_Saml2_Settings(json_settings)
-
response_5 = OneLogin_Saml2_Response(settings, xml_4)
- with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'NameID not found in the assertion of the Response'):
- response_5.get_nameid()
+ self.assertIsNone(response_5.get_nameid())
+ json_settings['strict'] = False
json_settings['security']['wantNameId'] = False
settings = OneLogin_Saml2_Settings(json_settings)
-
response_6 = OneLogin_Saml2_Response(settings, xml_4)
- nameid_6 = response_6.get_nameid()
- self.assertIsNone(nameid_6)
+ self.assertIsNone(response_6.get_nameid())
+
+ json_settings['security']['wantNameId'] = True
+ settings = OneLogin_Saml2_Settings(json_settings)
+ response_7 = OneLogin_Saml2_Response(settings, xml_4)
+ self.assertIsNone(response_7.get_nameid())
del json_settings['security']['wantNameId']
settings = OneLogin_Saml2_Settings(json_settings)
+ response_8 = OneLogin_Saml2_Response(settings, xml_4)
+ self.assertIsNone(response_8.get_nameid())
- response_7 = OneLogin_Saml2_Response(settings, xml_4)
- with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'NameID not found in the assertion of the Response'):
- response_7.get_nameid()
+ json_settings['strict'] = True
+ settings = OneLogin_Saml2_Settings(json_settings)
+ response_9 = OneLogin_Saml2_Response(settings, xml_4)
+ with self.assertRaisesRegexp(Exception, 'NameID not found in the assertion of the Response'):
+ response_9.get_nameid()
+
+ json_settings['strict'] = False
+ settings = OneLogin_Saml2_Settings(json_settings)
+ xml_5 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'wrong_spnamequalifier.xml.base64'))
+ response_10 = OneLogin_Saml2_Response(settings, xml_5)
+ self.assertEqual('test@example.com', response_10.get_nameid())
json_settings['strict'] = True
settings = OneLogin_Saml2_Settings(json_settings)
xml_5 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'wrong_spnamequalifier.xml.base64'))
- response_8 = OneLogin_Saml2_Response(settings, xml_5)
- with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'The SPNameQualifier value mistmatch the SP entityID value'):
- response_8.get_nameid()
+ response_11 = OneLogin_Saml2_Response(settings, xml_5)
+ with self.assertRaisesRegexp(Exception, 'The SPNameQualifier value mistmatch the SP entityID value.'):
+ response_11.get_nameid()
+
+ json_settings['strict'] = True
+ json_settings['security']['wantNameId'] = True
+ settings = OneLogin_Saml2_Settings(json_settings)
xml_6 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'empty_nameid.xml.base64'))
- response_9 = OneLogin_Saml2_Response(settings, xml_6)
- with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'An empty NameID value found'):
- response_9.get_nameid()
+ response_12 = OneLogin_Saml2_Response(settings, xml_6)
+ with self.assertRaisesRegexp(Exception, 'An empty NameID value found'):
+ response_12.get_nameid()
json_settings['security']['wantNameId'] = False
settings = OneLogin_Saml2_Settings(json_settings)
+ response_13 = OneLogin_Saml2_Response(settings, xml_6)
+ self.assertIsNone(response_13.get_nameid())
+
+ json_settings['strict'] = False
+ json_settings['security']['wantNameId'] = False
+ settings = OneLogin_Saml2_Settings(json_settings)
+ response_14 = OneLogin_Saml2_Response(settings, xml_6)
+ self.assertIsNone(response_14.get_nameid())
+
+ json_settings['security']['wantNameId'] = True
+ settings = OneLogin_Saml2_Settings(json_settings)
+ response_15 = OneLogin_Saml2_Response(settings, xml_6)
+ self.assertIsNone(response_15.get_nameid())
+
+ del json_settings['security']['wantNameId']
+ settings = OneLogin_Saml2_Settings(json_settings)
+ response_16 = OneLogin_Saml2_Response(settings, xml_6)
+ self.assertIsNone(response_16.get_nameid())
- nameid_9 = response_9.get_nameid()
- self.assertEqual(None, nameid_9)
+ json_settings['strict'] = True
+ settings = OneLogin_Saml2_Settings(json_settings)
+ response_17 = OneLogin_Saml2_Response(settings, xml_6)
+ with self.assertRaisesRegexp(Exception, 'An empty NameID value found'):
+ response_17.get_nameid()
def testReturnNameIdFormat(self):
"""
Tests the get_nameid_format method of the OneLogin_Saml2_Response
"""
json_settings = self.loadSettingsJSON()
- json_settings['strict'] = True
-
+ json_settings['strict'] = False
settings = OneLogin_Saml2_Settings(json_settings)
xml = self.file_contents(join(self.data_path, 'responses', 'response1.xml.base64'))
response = OneLogin_Saml2_Response(settings, xml)
@@ -162,59 +201,99 @@ def testReturnNameIdFormat(self):
response_3 = OneLogin_Saml2_Response(settings, xml_3)
self.assertEqual('urn:oasis:names:tc:SAML:2.0:nameid-format:transient', response_3.get_nameid_format())
+ json_settings['strict'] = True
+ json_settings['security']['wantNameId'] = True
+ settings = OneLogin_Saml2_Settings(json_settings)
+
xml_4 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_nameid.xml.base64'))
response_4 = OneLogin_Saml2_Response(settings, xml_4)
- with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'NameID not found in the assertion of the Response'):
+ with self.assertRaisesRegexp(Exception, 'NameID not found in the assertion of the Response'):
response_4.get_nameid_format()
- json_settings['security']['wantNameId'] = True
+ json_settings['security']['wantNameId'] = False
settings = OneLogin_Saml2_Settings(json_settings)
-
response_5 = OneLogin_Saml2_Response(settings, xml_4)
- with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'NameID not found in the assertion of the Response'):
- response_5.get_nameid_format()
+ self.assertIsNone(response_5.get_nameid_format())
+ json_settings['strict'] = False
json_settings['security']['wantNameId'] = False
settings = OneLogin_Saml2_Settings(json_settings)
-
response_6 = OneLogin_Saml2_Response(settings, xml_4)
- nameid_6 = response_6.get_nameid_format()
- self.assertIsNone(nameid_6)
+ self.assertIsNone(response_6.get_nameid_format())
+
+ json_settings['security']['wantNameId'] = True
+ settings = OneLogin_Saml2_Settings(json_settings)
+ response_7 = OneLogin_Saml2_Response(settings, xml_4)
+ self.assertIsNone(response_7.get_nameid_format())
del json_settings['security']['wantNameId']
settings = OneLogin_Saml2_Settings(json_settings)
+ response_8 = OneLogin_Saml2_Response(settings, xml_4)
+ self.assertIsNone(response_8.get_nameid_format())
- response_7 = OneLogin_Saml2_Response(settings, xml_4)
- with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'NameID not found in the assertion of the Response'):
- response_7.get_nameid_format()
+ json_settings['strict'] = True
+ settings = OneLogin_Saml2_Settings(json_settings)
+ response_9 = OneLogin_Saml2_Response(settings, xml_4)
+ with self.assertRaisesRegexp(Exception, 'NameID not found in the assertion of the Response'):
+ response_9.get_nameid_format()
+
+ json_settings['strict'] = False
+ settings = OneLogin_Saml2_Settings(json_settings)
+ xml_5 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'wrong_spnamequalifier.xml.base64'))
+ response_10 = OneLogin_Saml2_Response(settings, xml_5)
+ self.assertEqual('urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress', response_10.get_nameid_format())
json_settings['strict'] = True
settings = OneLogin_Saml2_Settings(json_settings)
xml_5 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'wrong_spnamequalifier.xml.base64'))
- response_8 = OneLogin_Saml2_Response(settings, xml_5)
- with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'The SPNameQualifier value mistmatch the SP entityID value'):
- response_8.get_nameid_format()
+ response_11 = OneLogin_Saml2_Response(settings, xml_5)
+ with self.assertRaisesRegexp(Exception, 'The SPNameQualifier value mistmatch the SP entityID value.'):
+ response_11.get_nameid_format()
+
+ json_settings['strict'] = True
+ json_settings['security']['wantNameId'] = True
+ settings = OneLogin_Saml2_Settings(json_settings)
xml_6 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'empty_nameid.xml.base64'))
- response_9 = OneLogin_Saml2_Response(settings, xml_6)
- with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'An empty NameID value found'):
- response_9.get_nameid_format()
+ response_12 = OneLogin_Saml2_Response(settings, xml_6)
+ with self.assertRaisesRegexp(Exception, 'An empty NameID value found'):
+ response_12.get_nameid_format()
+
+ json_settings['security']['wantNameId'] = False
+ settings = OneLogin_Saml2_Settings(json_settings)
+ response_13 = OneLogin_Saml2_Response(settings, xml_6)
+ self.assertEqual('urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress', response_13.get_nameid_format())
+ json_settings['strict'] = False
json_settings['security']['wantNameId'] = False
settings = OneLogin_Saml2_Settings(json_settings)
+ response_14 = OneLogin_Saml2_Response(settings, xml_6)
+ self.assertEqual('urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress', response_14.get_nameid_format())
+
+ json_settings['security']['wantNameId'] = True
+ settings = OneLogin_Saml2_Settings(json_settings)
+ response_15 = OneLogin_Saml2_Response(settings, xml_6)
+ self.assertEqual('urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress', response_15.get_nameid_format())
+
+ del json_settings['security']['wantNameId']
+ settings = OneLogin_Saml2_Settings(json_settings)
+ response_16 = OneLogin_Saml2_Response(settings, xml_6)
+ self.assertEqual('urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress', response_16.get_nameid_format())
- nameid_9 = response_9.get_nameid_format()
- self.assertEqual('urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress', nameid_9)
+ json_settings['strict'] = True
+ settings = OneLogin_Saml2_Settings(json_settings)
+ response_17 = OneLogin_Saml2_Response(settings, xml_6)
+ with self.assertRaisesRegexp(Exception, 'An empty NameID value found'):
+ response_17.get_nameid_format()
def testGetNameIdData(self):
"""
Tests the get_nameid_data method of the OneLogin_Saml2_Response
"""
json_settings = self.loadSettingsJSON()
- json_settings['strict'] = True
-
- settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
+ json_settings['strict'] = False
+ settings = OneLogin_Saml2_Settings(json_settings)
xml = self.file_contents(join(self.data_path, 'responses', 'response1.xml.base64'))
response = OneLogin_Saml2_Response(settings, xml)
expected_nameid_data = {
@@ -244,56 +323,110 @@ def testGetNameIdData(self):
nameid_data_3 = response_3.get_nameid_data()
self.assertEqual(expected_nameid_data_3, nameid_data_3)
+ json_settings['strict'] = True
+ json_settings['security']['wantNameId'] = True
+ settings = OneLogin_Saml2_Settings(json_settings)
+
xml_4 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_nameid.xml.base64'))
response_4 = OneLogin_Saml2_Response(settings, xml_4)
+ with self.assertRaisesRegexp(Exception, 'NameID not found in the assertion of the Response'):
+ response_4.get_nameid_data()
- nameid_data_4 = response_4.get_nameid_data()
- self.assertEqual({}, nameid_data_4)
-
- json_settings['security']['wantNameId'] = True
+ json_settings['security']['wantNameId'] = False
settings = OneLogin_Saml2_Settings(json_settings)
-
response_5 = OneLogin_Saml2_Response(settings, xml_4)
- with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'NameID not found in the assertion of the Response'):
- response_5.get_nameid_data()
+ nameid_data_5 = response_5.get_nameid_data()
+ self.assertEqual({}, nameid_data_5)
+ json_settings['strict'] = False
json_settings['security']['wantNameId'] = False
settings = OneLogin_Saml2_Settings(json_settings)
-
response_6 = OneLogin_Saml2_Response(settings, xml_4)
nameid_data_6 = response_6.get_nameid_data()
self.assertEqual({}, nameid_data_6)
+ json_settings['security']['wantNameId'] = True
+ settings = OneLogin_Saml2_Settings(json_settings)
+ response_7 = OneLogin_Saml2_Response(settings, xml_4)
+ nameid_data_7 = response_7.get_nameid_data()
+ self.assertEqual({}, nameid_data_7)
+
del json_settings['security']['wantNameId']
settings = OneLogin_Saml2_Settings(json_settings)
+ response_8 = OneLogin_Saml2_Response(settings, xml_4)
+ nameid_data_8 = response_8.get_nameid_data()
+ self.assertEqual({}, nameid_data_8)
- response_7 = OneLogin_Saml2_Response(settings, xml_4)
- with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'NameID not found in the assertion of the Response'):
- response_7.get_nameid_data()
+ json_settings['strict'] = True
+ settings = OneLogin_Saml2_Settings(json_settings)
+ response_9 = OneLogin_Saml2_Response(settings, xml_4)
+ with self.assertRaisesRegexp(Exception, 'NameID not found in the assertion of the Response'):
+ response_9.get_nameid_data()
+
+ expected_nameid_data_4 = {
+ 'Format': 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
+ 'SPNameQualifier': 'wrong-sp-entityid',
+ 'Value': 'test@example.com'
+ }
+ json_settings['strict'] = False
+ settings = OneLogin_Saml2_Settings(json_settings)
+ xml_5 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'wrong_spnamequalifier.xml.base64'))
+ response_10 = OneLogin_Saml2_Response(settings, xml_5)
+ nameid_data_10 = response_10.get_nameid_data()
+ self.assertEqual(expected_nameid_data_4, nameid_data_10)
json_settings['strict'] = True
settings = OneLogin_Saml2_Settings(json_settings)
xml_5 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'wrong_spnamequalifier.xml.base64'))
- response_8 = OneLogin_Saml2_Response(settings, xml_5)
- with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'The SPNameQualifier value mistmatch the SP entityID value.'):
- response_8.get_nameid_data()
+ response_11 = OneLogin_Saml2_Response(settings, xml_5)
+ with self.assertRaisesRegexp(Exception, 'The SPNameQualifier value mistmatch the SP entityID value.'):
+ response_11.get_nameid_data()
+
+ expected_nameid_data_5 = {
+ 'Value': None,
+ 'Format': 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
+ }
+
+ json_settings['strict'] = True
+ json_settings['security']['wantNameId'] = True
+ settings = OneLogin_Saml2_Settings(json_settings)
xml_6 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'empty_nameid.xml.base64'))
- response_9 = OneLogin_Saml2_Response(settings, xml_6)
- with self.assertRaisesRegexp(OneLogin_Saml2_ValidationError, 'An empty NameID value found'):
- response_9.get_nameid_data()
+ response_12 = OneLogin_Saml2_Response(settings, xml_6)
+ with self.assertRaisesRegexp(Exception, 'An empty NameID value found'):
+ response_12.get_nameid_data()
json_settings['security']['wantNameId'] = False
settings = OneLogin_Saml2_Settings(json_settings)
+ response_13 = OneLogin_Saml2_Response(settings, xml_6)
+ nameid_data_13 = response_13.get_nameid_data()
+ nameid_data_13 = self.assertEqual(expected_nameid_data_5, nameid_data_13)
- nameid_data_9 = response_9.get_nameid_data()
+ json_settings['strict'] = False
+ json_settings['security']['wantNameId'] = False
+ settings = OneLogin_Saml2_Settings(json_settings)
+ response_14 = OneLogin_Saml2_Response(settings, xml_6)
+ nameid_data_14 = response_14.get_nameid_data()
+ self.assertEqual(expected_nameid_data_5, nameid_data_14)
- expected_nameid_data_4 = {
- 'Value': None,
- 'Format': 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
- }
- self.assertEqual(expected_nameid_data_4, nameid_data_9)
+ json_settings['security']['wantNameId'] = True
+ settings = OneLogin_Saml2_Settings(json_settings)
+ response_15 = OneLogin_Saml2_Response(settings, xml_6)
+ nameid_data_15 = response_15.get_nameid_data()
+ self.assertEqual(expected_nameid_data_5, nameid_data_15)
+
+ del json_settings['security']['wantNameId']
+ settings = OneLogin_Saml2_Settings(json_settings)
+ response_16 = OneLogin_Saml2_Response(settings, xml_6)
+ nameid_data_16 = response_16.get_nameid_data()
+ self.assertEqual(expected_nameid_data_5, nameid_data_16)
+
+ json_settings['strict'] = True
+ settings = OneLogin_Saml2_Settings(json_settings)
+ response_17 = OneLogin_Saml2_Response(settings, xml_6)
+ with self.assertRaisesRegexp(Exception, 'An empty NameID value found'):
+ response_17.get_nameid_data()
def testCheckStatus(self):
"""
From b089e782ca954eedd2157dc5d5d7115ab1ed2fac Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Sat, 16 Sep 2017 08:30:45 +0200
Subject: [PATCH 123/255] Release 2.3.0
---
changelog.md | 9 +++++++--
setup.py | 2 +-
2 files changed, 8 insertions(+), 3 deletions(-)
diff --git a/changelog.md b/changelog.md
index bb3f142b..7fc8e5a9 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,6 +1,11 @@
# python-saml changelog
-### 2.2.4 (unreleased)
-* Get NameID when element decrypted twice
+### 2.3.0 (Sep 15, 2017)
+* [#205](https://github.com/onelogin/python-saml/pull/205) Improve decrypt method, Add an option to decrypt an element in place or copy it before decryption.
+* [#204](https://github.com/onelogin/python-saml/pull/204) On a LogoutRequest if the NameIdFormat is entity, NameQualifier and SPNameQualifier will be ommited. If the NameIdFormat is not entity and a NameQualifier is provided, then the SPNameQualifier will be also added.
+* Be able to get at the auth object the last processed ID (response/assertion) and the last generated ID.
+* Reset errorReason attribute of the auth object before each Process method
+* Fix issue on getting multiple certs when only sign or encryption certs
+* Allow empty nameid if setting wantNameId is false. Only raise Exceptions when strict mode is enabled
### 2.2.3 (Jun 15, 2017)
* Replace some etree.tostring calls, that were introduced recfently, by the sanitized call provided by defusedxml
diff --git a/setup.py b/setup.py
index 7e9b9a83..39e1be11 100644
--- a/setup.py
+++ b/setup.py
@@ -9,7 +9,7 @@
setup(
name='python-saml',
- version='2.2.3',
+ version='2.3.0',
description='Onelogin Python Toolkit. Add SAML support to your Python software using this library',
classifiers=[
'Development Status :: 5 - Production/Stable',
From 265d019451adac073ca8f470c757a05e9b447867 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Thu, 19 Oct 2017 12:10:39 +0200
Subject: [PATCH 124/255] Fix issue with LogoutRequest rejected by ADFS due
NameID with unspecified format instead no format attribute
---
src/onelogin/saml2/logout_request.py | 12 ++--
src/onelogin/saml2/utils.py | 5 +-
.../saml2_tests/logout_request_test.py | 57 +++++++++++++++----
tests/src/OneLogin/saml2_tests/utils_test.py | 11 ++++
4 files changed, 66 insertions(+), 19 deletions(-)
diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py
index 6e6a1903..2efd8aa8 100644
--- a/src/onelogin/saml2/logout_request.py
+++ b/src/onelogin/saml2/logout_request.py
@@ -76,15 +76,13 @@ def __init__(self, settings, request=None, name_id=None, session_index=None, nq=
cert = idp_data['x509cert']
if name_id is not None:
- if name_id_format is not None:
- nameIdFormat = name_id_format
- else:
- nameIdFormat = sp_data['NameIDFormat']
+ if not name_id_format and sp_data['NameIDFormat'] != OneLogin_Saml2_Constants.NAMEID_UNSPECIFIED:
+ name_id_format = sp_data['NameIDFormat']
else:
- nameIdFormat = OneLogin_Saml2_Constants.NAMEID_ENTITY
+ name_id_format = OneLogin_Saml2_Constants.NAMEID_ENTITY
spNameQualifier = None
- if nameIdFormat == OneLogin_Saml2_Constants.NAMEID_ENTITY:
+ if name_id_format == OneLogin_Saml2_Constants.NAMEID_ENTITY:
name_id = idp_data['entityId']
nq = None
elif nq is not None:
@@ -94,7 +92,7 @@ def __init__(self, settings, request=None, name_id=None, session_index=None, nq=
name_id_obj = OneLogin_Saml2_Utils.generate_name_id(
name_id,
spNameQualifier,
- nameIdFormat,
+ name_id_format,
cert,
False,
nq
diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py
index 19f1511a..51e5db18 100644
--- a/src/onelogin/saml2/utils.py
+++ b/src/onelogin/saml2/utils.py
@@ -612,7 +612,7 @@ def format_finger_print(fingerprint):
return formated_fingerprint.lower()
@staticmethod
- def generate_name_id(value, sp_nq, sp_format, cert=None, debug=False, nq=None):
+ def generate_name_id(value, sp_nq, sp_format=None, cert=None, debug=False, nq=None):
"""
Generates a nameID.
@@ -646,7 +646,8 @@ def generate_name_id(value, sp_nq, sp_format, cert=None, debug=False, nq=None):
name_id.setAttribute('SPNameQualifier', sp_nq)
if nq is not None:
name_id.setAttribute('NameQualifier', nq)
- name_id.setAttribute('Format', sp_format)
+ if sp_format is not None:
+ name_id.setAttribute('Format', sp_format)
name_id.appendChild(doc.createTextNode(value))
name_id_container.appendChild(name_id)
diff --git a/tests/src/OneLogin/saml2_tests/logout_request_test.py b/tests/src/OneLogin/saml2_tests/logout_request_test.py
index d1eab57e..6d78d1bc 100644
--- a/tests/src/OneLogin/saml2_tests/logout_request_test.py
+++ b/tests/src/OneLogin/saml2_tests/logout_request_test.py
@@ -55,15 +55,12 @@ def testConstructor(self):
inflated = OneLogin_Saml2_Utils.decode_base64_and_inflate(payload)
self.assertRegexpMatches(inflated, '^')
- def testCreateDeflatedSAMLLogoutRequestURLParameter(self):
+ def testConstructorWithNameIdFormatOnSettings(self):
"""
Tests the OneLogin_Saml2_LogoutRequest Constructor.
- The creation of a deflated SAML Logout Request
+ Case: Defines NameIDFormat from settings
"""
- settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
+ settings_info = self.loadSettingsJSON()
+ name_id = 'ONELOGIN_1e442c129e1f822c8096086a1103c5ee2c7cae1c'
+ name_id_format = 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'
+ settings_info['sp']['NameIDFormat'] = name_id_format
+ settings = OneLogin_Saml2_Settings(settings_info)
+ logout_request = OneLogin_Saml2_Logout_Request(settings, name_id=name_id)
+ logout_request_xml = OneLogin_Saml2_Utils.decode_base64_and_inflate(logout_request.get_request())
+ name_id_data = OneLogin_Saml2_Logout_Request.get_nameid_data(logout_request_xml)
+ expected_name_id_data = {
+ 'Value': name_id,
+ 'Format': name_id_format
+ }
+ self.assertEqual(expected_name_id_data, name_id_data)
+
+ def testConstructorWithoutNameIdFormat(self):
+ """
+ Tests the OneLogin_Saml2_LogoutRequest Constructor.
+ Case: Checks that NameIDFormat is not added
+ """
+ settings_info = self.loadSettingsJSON()
+ name_id = 'ONELOGIN_1e442c129e1f822c8096086a1103c5ee2c7cae1c'
+ name_id_format = 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified'
+ settings_info['sp']['NameIDFormat'] = name_id_format
+ settings = OneLogin_Saml2_Settings(settings_info)
+ logout_request = OneLogin_Saml2_Logout_Request(settings, name_id=name_id)
+ logout_request_xml = OneLogin_Saml2_Utils.decode_base64_and_inflate(logout_request.get_request())
+ name_id_data = OneLogin_Saml2_Logout_Request.get_nameid_data(logout_request_xml)
+ expected_name_id_data = {
+ 'Value': name_id
+ }
+ self.assertEqual(expected_name_id_data, name_id_data)
+
+ def testConstructorEncryptIdUsingX509certMulti(self):
+ """
+ Tests the OneLogin_Saml2_LogoutRequest Constructor.
+ Case: Able to generate encryptedID with MultiCert
+ """
+ settings_info = self.loadSettingsJSON('settings8.json')
+ settings_info['security']['nameIdEncrypted'] = True
+ settings = OneLogin_Saml2_Settings(settings_info)
+
logout_request = OneLogin_Saml2_Logout_Request(settings)
parameters = {'SAMLRequest': logout_request.get_request()}
@@ -92,6 +128,7 @@ def testCreateDeflatedSAMLLogoutRequestURLParameter(self):
payload = exploded['SAMLRequest'][0]
inflated = OneLogin_Saml2_Utils.decode_base64_and_inflate(payload)
self.assertRegexpMatches(inflated, '^')
def testGetIDFromSAMLLogoutRequest(self):
"""
diff --git a/tests/src/OneLogin/saml2_tests/utils_test.py b/tests/src/OneLogin/saml2_tests/utils_test.py
index cbc36d66..f1f2aeb6 100644
--- a/tests/src/OneLogin/saml2_tests/utils_test.py
+++ b/tests/src/OneLogin/saml2_tests/utils_test.py
@@ -607,6 +607,17 @@ def testGenerateNameIdWithSPNameQualifier(self):
expected_name_id_enc = ''
self.assertIn(expected_name_id_enc, name_id_enc)
+ def testGenerateNameIdWithoutFormat(self):
+ """
+ Tests the generateNameId method of the OneLogin_Saml2_Utils
+ """
+ name_id_value = 'ONELOGIN_ce998811003f4e60f8b07a311dc641621379cfde'
+ name_id_format = None
+
+ name_id = OneLogin_Saml2_Utils.generate_name_id(name_id_value, None, name_id_format)
+ expected_name_id = 'ONELOGIN_ce998811003f4e60f8b07a311dc641621379cfde '
+ self.assertEqual(name_id, expected_name_id)
+
def testGenerateNameIdWithoutSPNameQualifier(self):
"""
Tests the generateNameId method of the OneLogin_Saml2_Utils
From 032a2c7339c27788e795814512607c78482dd2ff Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Fri, 10 Nov 2017 21:36:20 +0100
Subject: [PATCH 125/255] Fix signature position in the SP metadata
---
src/onelogin/saml2/utils.py | 6 +++++-
tests/src/OneLogin/saml2_tests/utils_test.py | 18 +++++++++---------
2 files changed, 14 insertions(+), 10 deletions(-)
diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py
index 51e5db18..a9f49cd3 100644
--- a/src/onelogin/saml2/utils.py
+++ b/src/onelogin/saml2/utils.py
@@ -877,7 +877,11 @@ def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constant
issuer = issuer[0]
issuer.addnext(signature)
else:
- elem[0].insert(0, signature)
+ entity_descriptor = OneLogin_Saml2_Utils.query(elem, '//md:EntityDescriptor')
+ if len(entity_descriptor) > 0:
+ elem.insert(0, signature)
+ else:
+ elem[0].insert(0, signature)
digest_algorithm_transform_map = {
OneLogin_Saml2_Constants.SHA1: xmlsec.TransformSha1,
diff --git a/tests/src/OneLogin/saml2_tests/utils_test.py b/tests/src/OneLogin/saml2_tests/utils_test.py
index f1f2aeb6..0b9772f2 100644
--- a/tests/src/OneLogin/saml2_tests/utils_test.py
+++ b/tests/src/OneLogin/saml2_tests/utils_test.py
@@ -810,54 +810,54 @@ def testAddSign(self):
res = parseString(xml_authn_signed)
ds_signature = res.firstChild.firstChild.nextSibling.nextSibling
- self.assertIn('ds:Signature', ds_signature.tagName)
+ self.assertEqual('ds:Signature', ds_signature.tagName)
xml_authn_dom = parseString(xml_authn)
xml_authn_signed_2 = OneLogin_Saml2_Utils.add_sign(xml_authn_dom, key, cert)
self.assertIn('', xml_authn_signed_2)
res_2 = parseString(xml_authn_signed_2)
ds_signature_2 = res_2.firstChild.firstChild.nextSibling.nextSibling
- self.assertIn('ds:Signature', ds_signature_2.tagName)
+ self.assertEqual('ds:Signature', ds_signature_2.tagName)
xml_authn_signed_3 = OneLogin_Saml2_Utils.add_sign(xml_authn_dom.firstChild, key, cert)
self.assertIn('', xml_authn_signed_3)
res_3 = parseString(xml_authn_signed_3)
ds_signature_3 = res_3.firstChild.firstChild.nextSibling.nextSibling
- self.assertIn('ds:Signature', ds_signature_3.tagName)
+ self.assertEqual('ds:Signature', ds_signature_3.tagName)
xml_authn_etree = etree.fromstring(xml_authn)
xml_authn_signed_4 = OneLogin_Saml2_Utils.add_sign(xml_authn_etree, key, cert)
self.assertIn('', xml_authn_signed_4)
res_4 = parseString(xml_authn_signed_4)
ds_signature_4 = res_4.firstChild.firstChild.nextSibling.nextSibling
- self.assertIn('ds:Signature', ds_signature_4.tagName)
+ self.assertEqual('ds:Signature', ds_signature_4.tagName)
xml_authn_signed_5 = OneLogin_Saml2_Utils.add_sign(xml_authn_etree, key, cert)
self.assertIn('', xml_authn_signed_5)
res_5 = parseString(xml_authn_signed_5)
ds_signature_5 = res_5.firstChild.firstChild.nextSibling.nextSibling
- self.assertIn('ds:Signature', ds_signature_5.tagName)
+ self.assertEqual('ds:Signature', ds_signature_5.tagName)
xml_logout_req = b64decode(self.file_contents(join(self.data_path, 'logout_requests', 'logout_request.xml.base64')))
xml_logout_req_signed = OneLogin_Saml2_Utils.add_sign(xml_logout_req, key, cert)
self.assertIn('', xml_logout_req_signed)
res_6 = parseString(xml_logout_req_signed)
ds_signature_6 = res_6.firstChild.firstChild.nextSibling.nextSibling
- self.assertIn('ds:Signature', ds_signature_6.tagName)
+ self.assertEqual('ds:Signature', ds_signature_6.tagName)
xml_logout_res = b64decode(self.file_contents(join(self.data_path, 'logout_responses', 'logout_response.xml.base64')))
xml_logout_res_signed = OneLogin_Saml2_Utils.add_sign(xml_logout_res, key, cert)
self.assertIn('', xml_logout_res_signed)
res_7 = parseString(xml_logout_res_signed)
ds_signature_7 = res_7.firstChild.firstChild.nextSibling.nextSibling
- self.assertIn('ds:Signature', ds_signature_7.tagName)
+ self.assertEqual('ds:Signature', ds_signature_7.tagName)
xml_metadata = self.file_contents(join(self.data_path, 'metadata', 'metadata_settings1.xml'))
xml_metadata_signed = OneLogin_Saml2_Utils.add_sign(xml_metadata, key, cert)
self.assertIn('', xml_metadata_signed)
res_8 = parseString(xml_metadata_signed)
- ds_signature_8 = res_8.firstChild.firstChild.nextSibling.firstChild.nextSibling
- self.assertIn('ds:Signature', ds_signature_8.tagName)
+ ds_signature_8 = res_8.firstChild.firstChild.nextSibling
+ self.assertEqual('ds:Signature', ds_signature_8.tagName)
with self.assertRaisesRegexp(Exception, 'Error parsing xml string'):
OneLogin_Saml2_Utils.add_sign(1, key, cert)
From c8717ff5014ab73dfe7cf7617c49925d9d1e12bd Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Sun, 17 Dec 2017 20:57:53 +0100
Subject: [PATCH 126/255] Add more tests to cover IdPMetadataParser
---
...tadata_different_sign_and_encrypt_cert.xml | 72 +++++++++++++++++++
...dp_metadata_same_sign_and_encrypt_cert.xml | 71 ++++++++++++++++++
.../saml2_tests/idp_metadata_parser_test.py | 56 ++++++++++++++-
3 files changed, 198 insertions(+), 1 deletion(-)
create mode 100644 tests/data/metadata/idp_metadata_different_sign_and_encrypt_cert.xml
create mode 100644 tests/data/metadata/idp_metadata_same_sign_and_encrypt_cert.xml
diff --git a/tests/data/metadata/idp_metadata_different_sign_and_encrypt_cert.xml b/tests/data/metadata/idp_metadata_different_sign_and_encrypt_cert.xml
new file mode 100644
index 00000000..df90353a
--- /dev/null
+++ b/tests/data/metadata/idp_metadata_different_sign_and_encrypt_cert.xml
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+ MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzET
+MBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREwDwYD
+VQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbTAeFw0xMzA2
+MDUxNzE2MjBaFw0xODA2MDUxNzE2MjBaMGcxCzAJBgNVBAYTAlVTMRMwEQYDVQQI
+DApDYWxpZm9ybmlhMRUwEwYDVQQHDAxTYW50YSBNb25pY2ExETAPBgNVBAoMCE9u
+ZUxvZ2luMRkwFwYDVQQDDBBhcHAub25lbG9naW4uY29tMIIBIjANBgkqhkiG9w0B
+AQEFAAOCAQ8AMIIBCgKCAQEAse8rnep4qL2GmhH10pMQyJ2Jae+AQHyfgVjaQZ7Z
+0QQog5jX91vcJRSMi0XWJnUtOr6lF0dq1+yckjZ92wyLrH+7fvngNO1aV4Mjk9sT
+gf+iqMrae6y6fRxDt9PXrEFVjvd3vv7QTJf2FuIPy4vVP06Dt8EMkQIr8rmLmU0m
+Tr1k2DkrdtdlCuNFTXuAu3QqfvNCRrRwfNObn9MP6JeOUdcGLJsBjGF8exfcN1SF
+zRF0JFr3dmOlx761zK5liD0T1sYWnDquatj/JD9fZMbKecBKni1NglH/LVd+b6aJ
+UAr5LulERULUjLqYJRKW31u91/4Qazdo9tbvwqyFxaoUrwIDAQABo4HUMIHRMAwG
+A1UdEwEB/wQCMAAwHQYDVR0OBBYEFPWcXvQSlTXnzZD2xziuoUvrrDedMIGRBgNV
+HSMEgYkwgYaAFPWcXvQSlTXnzZD2xziuoUvrrDedoWukaTBnMQswCQYDVQQGEwJV
+UzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREw
+DwYDVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbYIBATAO
+BgNVHQ8BAf8EBAMCBPAwDQYJKoZIhvcNAQEFBQADggEBAB/8xe3rzqXQVxzHyAHu
+AuPa73ClDoL1cko0Fp8CGcqEIyj6Te9gx5z6wyfv+Lo8RFvBLlnB1lXqbC+fTGcV
+gG/4oKLJ5UwRFxInqpZPnOAudVNnd0PYOODn9FWs6u+OTIQIaIcPUv3MhB9lwHIJ
+sTk/bs9xcru5TPyLIxLLd6ib/pRceKH2mTkzUd0DYk9CQNXXeoGx/du5B9nh3ClP
+TbVakRzl3oswgI5MQIphYxkW70SopEh4kOFSRE1ND31NNIq1YrXlgtkguQBFsZWu
+QOPR6cEwFZzP0tHTYbI839WgxX6hfhIUTUz6mLqq4+3P4BG3+1OXeVDg63y8Uh78
+1sE=
+
+
+
+
+
+
+ MIIEZTCCA02gAwIBAgIUPyy/A3bZAZ4m28PzEUUoT7RJhxIwDQYJKoZIhvcNAQEF
+BQAwcjELMAkGA1UEBhMCVVMxKzApBgNVBAoMIk9uZUxvZ2luIFRlc3QgKHNnYXJj
+aWEtdXMtcHJlcHJvZCkxFTATBgNVBAsMDE9uZUxvZ2luIElkUDEfMB0GA1UEAwwW
+T25lTG9naW4gQWNjb3VudCA4OTE0NjAeFw0xNjA4MDQyMjI5MzdaFw0yMTA4MDUy
+MjI5MzdaMHIxCzAJBgNVBAYTAlVTMSswKQYDVQQKDCJPbmVMb2dpbiBUZXN0IChz
+Z2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQLDAxPbmVMb2dpbiBJZFAxHzAdBgNV
+BAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDYwggEiMA0GCSqGSIb3DQEBAQUAA4IB
+DwAwggEKAoIBAQDN6iqQGcLOCglNO42I2rkzE05UXSiMXT6c8ALThMMiaDw6qqzo
+3sd/tKK+NcNKWLIIC8TozWVyh5ykUiVZps+08xil7VsTU7E+wKu3kvmOsvw2wlRw
+tnoKZJwYhnr+RkBa+h1r3ZYUgXm1ZPeHMKj1g18KaWz9+MxYL6BhKqrOzfW/P2xx
+VRcFH7/pq+ZsDdgNzD2GD+apzY4MZyZj/N6BpBWJ0GlFsmtBegpbX3LBitJuFkk5
+L4/U/jjF1AJa3boBdCUVfATqO5G03H4XS1GySjBIRQXmlUF52rLjg6xCgWJ30/+t
+1X+IHLJeixiQ0vxyh6C4/usCEt94cgD1r8ADAgMBAAGjgfIwge8wDAYDVR0TAQH/
+BAIwADAdBgNVHQ4EFgQUPW0DcH0G3IwynWgi74co4wZ6n7gwga8GA1UdIwSBpzCB
+pIAUPW0DcH0G3IwynWgi74co4wZ6n7ihdqR0MHIxCzAJBgNVBAYTAlVTMSswKQYD
+VQQKDCJPbmVMb2dpbiBUZXN0IChzZ2FyY2lhLXVzLXByZXByb2QpMRUwEwYDVQQL
+DAxPbmVMb2dpbiBJZFAxHzAdBgNVBAMMFk9uZUxvZ2luIEFjY291bnQgODkxNDaC
+FD8svwN22QGeJtvD8xFFKE+0SYcSMA4GA1UdDwEB/wQEAwIHgDANBgkqhkiG9w0B
+AQUFAAOCAQEAQhB4q9jrycwbHrDSoYR1X4LFFzvJ9Us75wQquRHXpdyS9D6HUBXM
+GI6ahPicXCQrfLgN8vzMIiqZqfySXXv/8/dxe/X4UsWLYKYJHDJmxXD5EmWTa65c
+hjkeP1oJAc8f3CKCpcP2lOBTthbnk2fEVAeLHR4xNdQO0VvGXWO9BliYPpkYqUIB
+vlm+Fg9mF7AM/Uagq2503XXIE1Lq//HON68P10vNMwLSKOtYLsoTiCnuIKGJqG37
+MsZVjQ1ZPRcO+LSLkq0i91gFxrOrVCrgztX4JQi5XkvEsYZGIXXjwHqxTVyt3adZ
+WQO0LPxPqRiUqUzyhDhLo/xXNrHCu4VbMw==
+
+
+
+ urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress
+
+
+
+
+
+ Support
+ support@onelogin.com
+
+
\ No newline at end of file
diff --git a/tests/data/metadata/idp_metadata_same_sign_and_encrypt_cert.xml b/tests/data/metadata/idp_metadata_same_sign_and_encrypt_cert.xml
new file mode 100644
index 00000000..e7fd250b
--- /dev/null
+++ b/tests/data/metadata/idp_metadata_same_sign_and_encrypt_cert.xml
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+ MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzET
+MBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREwDwYD
+VQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbTAeFw0xMzA2
+MDUxNzE2MjBaFw0xODA2MDUxNzE2MjBaMGcxCzAJBgNVBAYTAlVTMRMwEQYDVQQI
+DApDYWxpZm9ybmlhMRUwEwYDVQQHDAxTYW50YSBNb25pY2ExETAPBgNVBAoMCE9u
+ZUxvZ2luMRkwFwYDVQQDDBBhcHAub25lbG9naW4uY29tMIIBIjANBgkqhkiG9w0B
+AQEFAAOCAQ8AMIIBCgKCAQEAse8rnep4qL2GmhH10pMQyJ2Jae+AQHyfgVjaQZ7Z
+0QQog5jX91vcJRSMi0XWJnUtOr6lF0dq1+yckjZ92wyLrH+7fvngNO1aV4Mjk9sT
+gf+iqMrae6y6fRxDt9PXrEFVjvd3vv7QTJf2FuIPy4vVP06Dt8EMkQIr8rmLmU0m
+Tr1k2DkrdtdlCuNFTXuAu3QqfvNCRrRwfNObn9MP6JeOUdcGLJsBjGF8exfcN1SF
+zRF0JFr3dmOlx761zK5liD0T1sYWnDquatj/JD9fZMbKecBKni1NglH/LVd+b6aJ
+UAr5LulERULUjLqYJRKW31u91/4Qazdo9tbvwqyFxaoUrwIDAQABo4HUMIHRMAwG
+A1UdEwEB/wQCMAAwHQYDVR0OBBYEFPWcXvQSlTXnzZD2xziuoUvrrDedMIGRBgNV
+HSMEgYkwgYaAFPWcXvQSlTXnzZD2xziuoUvrrDedoWukaTBnMQswCQYDVQQGEwJV
+UzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREw
+DwYDVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbYIBATAO
+BgNVHQ8BAf8EBAMCBPAwDQYJKoZIhvcNAQEFBQADggEBAB/8xe3rzqXQVxzHyAHu
+AuPa73ClDoL1cko0Fp8CGcqEIyj6Te9gx5z6wyfv+Lo8RFvBLlnB1lXqbC+fTGcV
+gG/4oKLJ5UwRFxInqpZPnOAudVNnd0PYOODn9FWs6u+OTIQIaIcPUv3MhB9lwHIJ
+sTk/bs9xcru5TPyLIxLLd6ib/pRceKH2mTkzUd0DYk9CQNXXeoGx/du5B9nh3ClP
+TbVakRzl3oswgI5MQIphYxkW70SopEh4kOFSRE1ND31NNIq1YrXlgtkguQBFsZWu
+QOPR6cEwFZzP0tHTYbI839WgxX6hfhIUTUz6mLqq4+3P4BG3+1OXeVDg63y8Uh78
+1sE=
+
+
+
+
+
+
+ MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzET
+MBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREwDwYD
+VQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbTAeFw0xMzA2
+MDUxNzE2MjBaFw0xODA2MDUxNzE2MjBaMGcxCzAJBgNVBAYTAlVTMRMwEQYDVQQI
+DApDYWxpZm9ybmlhMRUwEwYDVQQHDAxTYW50YSBNb25pY2ExETAPBgNVBAoMCE9u
+ZUxvZ2luMRkwFwYDVQQDDBBhcHAub25lbG9naW4uY29tMIIBIjANBgkqhkiG9w0B
+AQEFAAOCAQ8AMIIBCgKCAQEAse8rnep4qL2GmhH10pMQyJ2Jae+AQHyfgVjaQZ7Z
+0QQog5jX91vcJRSMi0XWJnUtOr6lF0dq1+yckjZ92wyLrH+7fvngNO1aV4Mjk9sT
+gf+iqMrae6y6fRxDt9PXrEFVjvd3vv7QTJf2FuIPy4vVP06Dt8EMkQIr8rmLmU0m
+Tr1k2DkrdtdlCuNFTXuAu3QqfvNCRrRwfNObn9MP6JeOUdcGLJsBjGF8exfcN1SF
+zRF0JFr3dmOlx761zK5liD0T1sYWnDquatj/JD9fZMbKecBKni1NglH/LVd+b6aJ
+UAr5LulERULUjLqYJRKW31u91/4Qazdo9tbvwqyFxaoUrwIDAQABo4HUMIHRMAwG
+A1UdEwEB/wQCMAAwHQYDVR0OBBYEFPWcXvQSlTXnzZD2xziuoUvrrDedMIGRBgNV
+HSMEgYkwgYaAFPWcXvQSlTXnzZD2xziuoUvrrDedoWukaTBnMQswCQYDVQQGEwJV
+UzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREw
+DwYDVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbYIBATAO
+BgNVHQ8BAf8EBAMCBPAwDQYJKoZIhvcNAQEFBQADggEBAB/8xe3rzqXQVxzHyAHu
+AuPa73ClDoL1cko0Fp8CGcqEIyj6Te9gx5z6wyfv+Lo8RFvBLlnB1lXqbC+fTGcV
+gG/4oKLJ5UwRFxInqpZPnOAudVNnd0PYOODn9FWs6u+OTIQIaIcPUv3MhB9lwHIJ
+sTk/bs9xcru5TPyLIxLLd6ib/pRceKH2mTkzUd0DYk9CQNXXeoGx/du5B9nh3ClP
+TbVakRzl3oswgI5MQIphYxkW70SopEh4kOFSRE1ND31NNIq1YrXlgtkguQBFsZWu
+QOPR6cEwFZzP0tHTYbI839WgxX6hfhIUTUz6mLqq4+3P4BG3+1OXeVDg63y8Uh78
+1sE=
+
+
+
+ urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress
+
+
+
+
+
+ Support
+ support@onelogin.com
+
+
\ No newline at end of file
diff --git a/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py b/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py
index 58e2852e..e441e0f0 100644
--- a/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py
+++ b/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py
@@ -389,7 +389,61 @@ def test_parse_multi_singing_certs(self):
expected_settings = json.loads(expected_settings_json)
self.assertEqual(expected_settings, data)
- def testMergeSettings(self):
+ def test_parse_multi_same_signing_and_encrypt_cert(self):
+ """
+ Tests the parse method of the OneLogin_Saml2_IdPMetadataParser
+ Case: IdP metadata contains multiple signature cert and encrypt cert
+ that is the same
+ """
+ xml_idp_metadata = self.file_contents(join(self.data_path, 'metadata', 'idp_metadata_same_sign_and_encrypt_cert.xml'))
+ data = OneLogin_Saml2_IdPMetadataParser.parse(xml_idp_metadata)
+
+ expected_settings_json = """
+ {
+ "sp": {
+ "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
+ },
+ "idp": {
+ "x509cert": "MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREwDwYDVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbTAeFw0xMzA2MDUxNzE2MjBaFw0xODA2MDUxNzE2MjBaMGcxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRUwEwYDVQQHDAxTYW50YSBNb25pY2ExETAPBgNVBAoMCE9uZUxvZ2luMRkwFwYDVQQDDBBhcHAub25lbG9naW4uY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAse8rnep4qL2GmhH10pMQyJ2Jae+AQHyfgVjaQZ7Z0QQog5jX91vcJRSMi0XWJnUtOr6lF0dq1+yckjZ92wyLrH+7fvngNO1aV4Mjk9sTgf+iqMrae6y6fRxDt9PXrEFVjvd3vv7QTJf2FuIPy4vVP06Dt8EMkQIr8rmLmU0mTr1k2DkrdtdlCuNFTXuAu3QqfvNCRrRwfNObn9MP6JeOUdcGLJsBjGF8exfcN1SFzRF0JFr3dmOlx761zK5liD0T1sYWnDquatj/JD9fZMbKecBKni1NglH/LVd+b6aJUAr5LulERULUjLqYJRKW31u91/4Qazdo9tbvwqyFxaoUrwIDAQABo4HUMIHRMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFPWcXvQSlTXnzZD2xziuoUvrrDedMIGRBgNVHSMEgYkwgYaAFPWcXvQSlTXnzZD2xziuoUvrrDedoWukaTBnMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREwDwYDVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbYIBATAOBgNVHQ8BAf8EBAMCBPAwDQYJKoZIhvcNAQEFBQADggEBAB/8xe3rzqXQVxzHyAHuAuPa73ClDoL1cko0Fp8CGcqEIyj6Te9gx5z6wyfv+Lo8RFvBLlnB1lXqbC+fTGcVgG/4oKLJ5UwRFxInqpZPnOAudVNnd0PYOODn9FWs6u+OTIQIaIcPUv3MhB9lwHIJsTk/bs9xcru5TPyLIxLLd6ib/pRceKH2mTkzUd0DYk9CQNXXeoGx/du5B9nh3ClPTbVakRzl3oswgI5MQIphYxkW70SopEh4kOFSRE1ND31NNIq1YrXlgtkguQBFsZWuQOPR6cEwFZzP0tHTYbI839WgxX6hfhIUTUz6mLqq4+3P4BG3+1OXeVDg63y8Uh781sE=",
+ "entityId": "https://app.onelogin.com/saml/metadata/383123",
+ "singleSignOnService": {
+ "url": "https://app.onelogin.com/trust/saml2/http-post/sso/383123",
+ "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
+ }
+ }
+ }
+ """
+ expected_settings = json.loads(expected_settings_json)
+ self.assertEqual(expected_settings, data)
+
+ xml_idp_metadata_2 = self.file_contents(join(self.data_path, 'metadata', 'idp_metadata_different_sign_and_encrypt_cert.xml'))
+ data_2 = OneLogin_Saml2_IdPMetadataParser.parse(xml_idp_metadata_2)
+ expected_settings_json_2 = """
+ {
+ "sp": {
+ "NameIDFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"
+ },
+ "idp": {
+ "x509certMulti": {
+ "encryption": [
+ "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=="
+ ],
+ "signing": [
+ "MIIEHjCCAwagAwIBAgIBATANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREwDwYDVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbTAeFw0xMzA2MDUxNzE2MjBaFw0xODA2MDUxNzE2MjBaMGcxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRUwEwYDVQQHDAxTYW50YSBNb25pY2ExETAPBgNVBAoMCE9uZUxvZ2luMRkwFwYDVQQDDBBhcHAub25lbG9naW4uY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAse8rnep4qL2GmhH10pMQyJ2Jae+AQHyfgVjaQZ7Z0QQog5jX91vcJRSMi0XWJnUtOr6lF0dq1+yckjZ92wyLrH+7fvngNO1aV4Mjk9sTgf+iqMrae6y6fRxDt9PXrEFVjvd3vv7QTJf2FuIPy4vVP06Dt8EMkQIr8rmLmU0mTr1k2DkrdtdlCuNFTXuAu3QqfvNCRrRwfNObn9MP6JeOUdcGLJsBjGF8exfcN1SFzRF0JFr3dmOlx761zK5liD0T1sYWnDquatj/JD9fZMbKecBKni1NglH/LVd+b6aJUAr5LulERULUjLqYJRKW31u91/4Qazdo9tbvwqyFxaoUrwIDAQABo4HUMIHRMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFPWcXvQSlTXnzZD2xziuoUvrrDedMIGRBgNVHSMEgYkwgYaAFPWcXvQSlTXnzZD2xziuoUvrrDedoWukaTBnMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEVMBMGA1UEBwwMU2FudGEgTW9uaWNhMREwDwYDVQQKDAhPbmVMb2dpbjEZMBcGA1UEAwwQYXBwLm9uZWxvZ2luLmNvbYIBATAOBgNVHQ8BAf8EBAMCBPAwDQYJKoZIhvcNAQEFBQADggEBAB/8xe3rzqXQVxzHyAHuAuPa73ClDoL1cko0Fp8CGcqEIyj6Te9gx5z6wyfv+Lo8RFvBLlnB1lXqbC+fTGcVgG/4oKLJ5UwRFxInqpZPnOAudVNnd0PYOODn9FWs6u+OTIQIaIcPUv3MhB9lwHIJsTk/bs9xcru5TPyLIxLLd6ib/pRceKH2mTkzUd0DYk9CQNXXeoGx/du5B9nh3ClPTbVakRzl3oswgI5MQIphYxkW70SopEh4kOFSRE1ND31NNIq1YrXlgtkguQBFsZWuQOPR6cEwFZzP0tHTYbI839WgxX6hfhIUTUz6mLqq4+3P4BG3+1OXeVDg63y8Uh781sE="
+ ]
+ },
+ "entityId": "https://app.onelogin.com/saml/metadata/383123",
+ "singleSignOnService": {
+ "url": "https://app.onelogin.com/trust/saml2/http-post/sso/383123",
+ "binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
+ }
+ }
+ }
+ """
+ expected_settings_2 = json.loads(expected_settings_json_2)
+ self.assertEqual(expected_settings_2, data_2)
+
+ def test_merge_settings(self):
"""
Tests the merge_settings method of the OneLogin_Saml2_IdPMetadataParser
"""
From 6b9faf5c6d140635d3fefcadd5319ee128abc529 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Mon, 18 Dec 2017 10:19:02 +0100
Subject: [PATCH 127/255] Redefine NSMAP constant
---
src/onelogin/saml2/constants.py | 27 ++++++++++++++++++---------
src/onelogin/saml2/response.py | 4 ++--
2 files changed, 20 insertions(+), 11 deletions(-)
diff --git a/src/onelogin/saml2/constants.py b/src/onelogin/saml2/constants.py
index 2cd2e340..2bf885ef 100644
--- a/src/onelogin/saml2/constants.py
+++ b/src/onelogin/saml2/constants.py
@@ -50,6 +50,24 @@ class OneLogin_Saml2_Constants(object):
NS_XENC = 'http://www.w3.org/2001/04/xmlenc#'
NS_DS = 'http://www.w3.org/2000/09/xmldsig#'
+ # Namespace Prefixes
+ NS_PREFIX_SAML = 'saml'
+ NS_PREFIX_SAMLP = 'samlp'
+ NS_PREFIX_MD = 'md'
+ NS_PREFIX_XS = 'xs'
+ NS_PREFIX_XSI = 'xsi'
+ NS_PREFIX_XENC = 'xenc'
+ NS_PREFIX_DS = 'ds'
+
+ # Prefix:Namespace Mappings
+ NSMAP = {
+ NS_PREFIX_SAMLP: NS_SAMLP,
+ NS_PREFIX_SAML: NS_SAML,
+ NS_PREFIX_DS: NS_DS,
+ NS_PREFIX_XENC: NS_XENC,
+ NS_PREFIX_MD: NS_MD
+ }
+
# Bindings
BINDING_HTTP_POST = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'
BINDING_HTTP_REDIRECT = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'
@@ -79,15 +97,6 @@ class OneLogin_Saml2_Constants(object):
STATUS_PARTIAL_LOGOUT = 'urn:oasis:names:tc:SAML:2.0:status:PartialLogout'
STATUS_PROXY_COUNT_EXCEEDED = 'urn:oasis:names:tc:SAML:2.0:status:ProxyCountExceeded'
- # Namespaces
- NSMAP = {
- 'samlp': NS_SAMLP,
- 'saml': NS_SAML,
- 'md': NS_MD,
- 'ds': NS_DS,
- 'xenc': NS_XENC
- }
-
# Sign & Crypto
SHA1 = 'http://www.w3.org/2000/09/xmldsig#sha1'
SHA256 = 'http://www.w3.org/2001/04/xmlenc#sha256'
diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py
index 869a4e7a..30def415 100644
--- a/src/onelogin/saml2/response.py
+++ b/src/onelogin/saml2/response.py
@@ -531,7 +531,7 @@ def get_attributes(self):
)
values = []
- for attr in attribute_node.iterchildren('{%s}AttributeValue' % OneLogin_Saml2_Constants.NSMAP['saml']):
+ for attr in attribute_node.iterchildren('{%s}AttributeValue' % OneLogin_Saml2_Constants.NSMAP[OneLogin_Saml2_Constants.NS_PREFIX_SAML]):
# Remove any whitespace (which may be present where attributes are
# nested inside NameID children).
if attr.text:
@@ -540,7 +540,7 @@ def get_attributes(self):
values.append(text)
# Parse any nested NameID children
- for nameid in attr.iterchildren('{%s}NameID' % OneLogin_Saml2_Constants.NSMAP['saml']):
+ for nameid in attr.iterchildren('{%s}NameID' % OneLogin_Saml2_Constants.NSMAP[OneLogin_Saml2_Constants.NS_PREFIX_SAML]):
values.append({
'NameID': {
'Format': nameid.get('Format'),
From 4081893698abc46c41e2a5c4b91bcdd37d401a19 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Mon, 18 Dec 2017 19:08:31 +0100
Subject: [PATCH 128/255] Be able to invalidate a SAMLResponse if it contains
InResponseTo value but no RequestId parameter provided at the is_valid
method. See rejectUnsolicitedResponsesWithInResponseTo security parameter (By
default deactivated)
---
README.md | 5 ++
src/onelogin/saml2/response.py | 23 +++++----
src/onelogin/saml2/settings.py | 3 ++
.../src/OneLogin/saml2_tests/response_test.py | 48 +++++++++++++++++++
4 files changed, 71 insertions(+), 8 deletions(-)
diff --git a/README.md b/README.md
index aa740fce..10ade02f 100644
--- a/README.md
+++ b/README.md
@@ -414,6 +414,11 @@ In addition to the required settings data (idp, sp), extra settings can be defin
// Indicates a requirement for the AttributeStatement element
"wantAttributeStatement": true,
+ // Rejects SAML responses with a InResponseTo attribute when request_id
+ // not provided in the process_response method that later call the
+ // response is_valid method with that parameter.
+ "rejectUnsolicitedResponsesWithInResponseTo": false,
+
// Authentication context.
// Set to false and no AuthContext will be sent in the AuthNRequest,
// Set true or don't present this parameter and you will get an AuthContext 'exact' 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport'
diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py
index 30def415..fe6f977e 100644
--- a/src/onelogin/saml2/response.py
+++ b/src/onelogin/saml2/response.py
@@ -133,14 +133,19 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False):
security = self.__settings.get_security_data()
current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data)
- # Check if the InResponseTo of the Response matchs the ID of the AuthNRequest (requestId) if provided
in_response_to = self.document.get('InResponseTo', None)
- if in_response_to is not None and request_id is not None:
- if in_response_to != request_id:
- raise OneLogin_Saml2_ValidationError(
- 'The InResponseTo of the Response: %s, does not match the ID of the AuthNRequest sent by the SP: %s' % (in_response_to, request_id),
- OneLogin_Saml2_ValidationError.WRONG_INRESPONSETO
- )
+ if request_id is None and in_response_to is not None and security.get('rejectUnsolicitedResponsesWithInResponseTo', False):
+ raise OneLogin_Saml2_ValidationError(
+ 'The Response has an InResponseTo attribute: %s while no InResponseTo was expected' % in_response_to,
+ OneLogin_Saml2_ValidationError.WRONG_INRESPONSETO
+ )
+
+ # Check if the InResponseTo of the Response matchs the ID of the AuthNRequest (requestId) if provided
+ if request_id is not None and in_response_to != request_id:
+ raise OneLogin_Saml2_ValidationError(
+ 'The InResponseTo of the Response: %s, does not match the ID of the AuthNRequest sent by the SP: %s' % (in_response_to, request_id),
+ OneLogin_Saml2_ValidationError.WRONG_INRESPONSETO
+ )
if not self.encrypted and security.get('wantAssertionsEncrypted', False):
raise OneLogin_Saml2_ValidationError(
@@ -244,7 +249,9 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False):
continue
else:
irt = sc_data.get('InResponseTo', None)
- if in_response_to and irt and irt != in_response_to:
+ if (in_response_to is None and irt is not None and
+ security.get('rejectUnsolicitedResponsesWithInResponseTo', False)) or \
+ in_response_to and irt and irt != in_response_to:
continue
recipient = sc_data.get('Recipient', None)
if recipient and current_url not in recipient:
diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py
index 77a8592a..d5db3a05 100644
--- a/src/onelogin/saml2/settings.py
+++ b/src/onelogin/saml2/settings.py
@@ -280,6 +280,9 @@ def __add_default_values(self):
# NameID element expected
self.__security.setdefault('wantNameId', True)
+ # SAML responses with a InResponseTo attribute not rejected when requestId not passed
+ self.__security.setdefault('rejectUnsolicitedResponsesWithInResponseTo', False)
+
# Encrypt expected
self.__security.setdefault('wantAssertionsEncrypted', False)
self.__security.setdefault('wantNameIdEncrypted', False)
diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py
index 6e4c1df8..8df0ddfd 100644
--- a/tests/src/OneLogin/saml2_tests/response_test.py
+++ b/tests/src/OneLogin/saml2_tests/response_test.py
@@ -1173,6 +1173,54 @@ def testIsInValidRequestId(self):
response.is_valid(request_data, valid_request_id)
self.assertEqual('No Signature found. SAML Response rejected', response.get_error())
+ def testRejectUnsolicitedResponsesWithInResponseTo(self):
+ settings_info = self.loadSettingsJSON()
+ settings_info['strict'] = True
+ settings_info['security']['rejectUnsolicitedResponsesWithInResponseTo'] = False
+ settings = OneLogin_Saml2_Settings(settings_info)
+ request_data = {
+ 'http_host': 'stuff.com',
+ 'script_name': 'endpoints/endpoints/acs.php'
+ }
+
+ xml = self.file_contents(join(self.data_path, 'responses', 'unsigned_response.xml.base64'))
+ response = OneLogin_Saml2_Response(settings, xml)
+ response.is_valid(request_data)
+ self.assertEqual('No Signature found. SAML Response rejected', response.get_error())
+
+ settings_info['security']['rejectUnsolicitedResponsesWithInResponseTo'] = True
+ settings = OneLogin_Saml2_Settings(settings_info)
+ response = OneLogin_Saml2_Response(settings, xml)
+ response.is_valid(request_data)
+ self.assertEqual('The Response has an InResponseTo attribute: _57bcbf70-7b1f-012e-c821-782bcb13bb38 while no InResponseTo was expected', response.get_error())
+
+ settings_info['idp']['entityId'] = 'https://pitbulk.no-ip.org/simplesaml/saml2/idp/metadata.php'
+ settings_info['sp']['entityId'] = 'https://pitbulk.no-ip.org/newonelogin/demo1/metadata.php'
+ request_data = {
+ 'https': 'on',
+ 'http_host': 'pitbulk.no-ip.org',
+ 'script_name': 'newonelogin/demo1/index.php?acs'
+ }
+ not_on_or_after = datetime.strptime('2014-02-19T09:37:01Z', '%Y-%m-%dT%H:%M:%SZ')
+ not_on_or_after -= timedelta(seconds=150)
+
+ # InResponseTo on the SubjectConfirmation only
+ xml = self.file_contents(join(self.data_path, 'responses', 'valid_response_without_inresponseto.xml.base64'))
+ settings_info['security']['rejectUnsolicitedResponsesWithInResponseTo'] = False
+ settings = OneLogin_Saml2_Settings(settings_info)
+ response = OneLogin_Saml2_Response(settings, xml)
+
+ with freeze_time(not_on_or_after):
+ self.assertTrue(response.is_valid(request_data))
+
+ settings_info['security']['rejectUnsolicitedResponsesWithInResponseTo'] = True
+ settings = OneLogin_Saml2_Settings(settings_info)
+ response = OneLogin_Saml2_Response(settings, xml)
+
+ with freeze_time(not_on_or_after):
+ self.assertFalse(response.is_valid(request_data))
+ self.assertEquals("A valid SubjectConfirmation was not found on this Response", response.get_error())
+
def testIsInValidSignIssues(self):
"""
Tests the is_valid method of the OneLogin_Saml2_Response class
From fad881b4432febea69d70691dfed51c93f0de10f Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Tue, 27 Feb 2018 14:12:18 +0100
Subject: [PATCH 129/255] Fix vulnerability CVE-2017-11427. Process text of
nodes properly, ignoring comments
---
src/onelogin/saml2/idp_metadata_parser.py | 6 +++---
src/onelogin/saml2/logout_request.py | 6 +++---
src/onelogin/saml2/logout_response.py | 2 +-
src/onelogin/saml2/response.py | 21 ++++++++++---------
src/onelogin/saml2/utils.py | 9 ++++++--
.../response_node_text_attack.xml.base64 | 1 +
.../src/OneLogin/saml2_tests/response_test.py | 13 ++++++++++++
7 files changed, 39 insertions(+), 19 deletions(-)
create mode 100644 tests/data/responses/response_node_text_attack.xml.base64
diff --git a/src/onelogin/saml2/idp_metadata_parser.py b/src/onelogin/saml2/idp_metadata_parser.py
index 097b3342..51d446ff 100644
--- a/src/onelogin/saml2/idp_metadata_parser.py
+++ b/src/onelogin/saml2/idp_metadata_parser.py
@@ -146,7 +146,7 @@ def parse(
name_id_format_nodes = OneLogin_Saml2_Utils.query(idp_descriptor_node, './md:NameIDFormat')
if len(name_id_format_nodes) > 0:
- idp_name_id_format = name_id_format_nodes[0].text
+ idp_name_id_format = OneLogin_Saml2_Utils.element_text(name_id_format_nodes[0])
sso_nodes = OneLogin_Saml2_Utils.query(
idp_descriptor_node,
@@ -171,11 +171,11 @@ def parse(
if len(signing_nodes) > 0:
certs['signing'] = []
for cert_node in signing_nodes:
- certs['signing'].append(''.join(cert_node.text.split()))
+ certs['signing'].append(''.join(OneLogin_Saml2_Utils.element_text(cert_node).split()))
if len(encryption_nodes) > 0:
certs['encryption'] = []
for cert_node in encryption_nodes:
- certs['encryption'].append(''.join(cert_node.text.split()))
+ certs['encryption'].append(''.join(OneLogin_Saml2_Utils.element_text(cert_node).split()))
data['idp'] = {}
diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py
index 2efd8aa8..14c49e29 100644
--- a/src/onelogin/saml2/logout_request.py
+++ b/src/onelogin/saml2/logout_request.py
@@ -218,7 +218,7 @@ def get_nameid_data(request, key=None):
)
name_id_data = {
- 'Value': name_id.text
+ 'Value': OneLogin_Saml2_Utils.element_text(name_id)
}
for attr in ['Format', 'SPNameQualifier', 'NameQualifier']:
if attr in name_id.attrib.keys():
@@ -276,7 +276,7 @@ def get_issuer(request):
issuer = None
issuer_nodes = OneLogin_Saml2_Utils.query(elem, '/samlp:LogoutRequest/saml:Issuer')
if len(issuer_nodes) == 1:
- issuer = issuer_nodes[0].text
+ issuer = OneLogin_Saml2_Utils.element_text(issuer_nodes[0])
return issuer
@staticmethod
@@ -298,7 +298,7 @@ def get_session_indexes(request):
session_indexes = []
session_index_nodes = OneLogin_Saml2_Utils.query(elem, '/samlp:LogoutRequest/samlp:SessionIndex')
for session_index_node in session_index_nodes:
- session_indexes.append(session_index_node.text)
+ session_indexes.append(OneLogin_Saml2_Utils.element_text(session_index_node))
return session_indexes
def is_valid(self, request_data, raise_exceptions=False):
diff --git a/src/onelogin/saml2/logout_response.py b/src/onelogin/saml2/logout_response.py
index eec5cc94..0cd1e2be 100644
--- a/src/onelogin/saml2/logout_response.py
+++ b/src/onelogin/saml2/logout_response.py
@@ -56,7 +56,7 @@ def get_issuer(self):
issuer = None
issuer_nodes = self.__query('/samlp:LogoutResponse/saml:Issuer')
if len(issuer_nodes) == 1:
- issuer = issuer_nodes[0].text
+ issuer = OneLogin_Saml2_Utils.element_text(issuer_nodes[0])
return issuer
def get_status(self):
diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py
index fe6f977e..56a8476b 100644
--- a/src/onelogin/saml2/response.py
+++ b/src/onelogin/saml2/response.py
@@ -377,7 +377,7 @@ def get_audiences(self):
:rtype: list
"""
audience_nodes = self.__query_assertion('/saml:Conditions/saml:AudienceRestriction/saml:Audience')
- return [node.text for node in audience_nodes if node.text is not None]
+ return [OneLogin_Saml2_Utils.element_text(node) for node in audience_nodes if OneLogin_Saml2_Utils.element_text(node) is not None]
def get_issuers(self):
"""
@@ -391,7 +391,7 @@ def get_issuers(self):
message_issuer_nodes = OneLogin_Saml2_Utils.query(self.document, '/samlp:Response/saml:Issuer')
if len(message_issuer_nodes) > 0:
if len(message_issuer_nodes) == 1:
- issuers.append(message_issuer_nodes[0].text)
+ issuers.append(OneLogin_Saml2_Utils.element_text(message_issuer_nodes[0]))
else:
raise OneLogin_Saml2_ValidationError(
'Issuer of the Response is multiple.',
@@ -400,7 +400,7 @@ def get_issuers(self):
assertion_issuer_nodes = self.__query_assertion('/saml:Issuer')
if len(assertion_issuer_nodes) == 1:
- issuers.append(assertion_issuer_nodes[0].text)
+ issuers.append(OneLogin_Saml2_Utils.element_text(assertion_issuer_nodes[0]))
else:
raise OneLogin_Saml2_ValidationError(
'Issuer of the Assertion not found or multiple.',
@@ -438,13 +438,13 @@ def get_nameid_data(self):
OneLogin_Saml2_ValidationError.NO_NAMEID
)
else:
- if is_strict and want_nameid and not nameid.text:
+ if is_strict and want_nameid and not OneLogin_Saml2_Utils.element_text(nameid):
raise OneLogin_Saml2_ValidationError(
'An empty NameID value found',
OneLogin_Saml2_ValidationError.EMPTY_NAMEID
)
- nameid_data = {'Value': nameid.text}
+ nameid_data = {'Value': OneLogin_Saml2_Utils.element_text(nameid)}
for attr in ['Format', 'SPNameQualifier', 'NameQualifier']:
value = nameid.get(attr, None)
if value:
@@ -541,10 +541,11 @@ def get_attributes(self):
for attr in attribute_node.iterchildren('{%s}AttributeValue' % OneLogin_Saml2_Constants.NSMAP[OneLogin_Saml2_Constants.NS_PREFIX_SAML]):
# Remove any whitespace (which may be present where attributes are
# nested inside NameID children).
- if attr.text:
- text = attr.text.strip()
- if text:
- values.append(text)
+ attr_text = OneLogin_Saml2_Utils.element_text(attr)
+ if attr_text:
+ attr_text = attr_text.strip()
+ if attr_text:
+ values.append(attr_text)
# Parse any nested NameID children
for nameid in attr.iterchildren('{%s}NameID' % OneLogin_Saml2_Constants.NSMAP[OneLogin_Saml2_Constants.NS_PREFIX_SAML]):
@@ -552,7 +553,7 @@ def get_attributes(self):
'NameID': {
'Format': nameid.get('Format'),
'NameQualifier': nameid.get('NameQualifier'),
- 'value': nameid.text
+ 'value': OneLogin_Saml2_Utils.element_text(nameid)
}
})
diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py
index a9f49cd3..a9ece8ba 100644
--- a/src/onelogin/saml2/utils.py
+++ b/src/onelogin/saml2/utils.py
@@ -156,6 +156,11 @@ def validate_xml(xml, schema, debug=False):
return parseString(tostring(dom, encoding='unicode').encode('utf-8'))
+ @staticmethod
+ def element_text(node):
+ etree.strip_tags(node, etree.Comment)
+ return node.text
+
@staticmethod
def format_cert(cert, heads=True):
"""
@@ -743,7 +748,7 @@ def get_status(dom):
if len(subcode_entry) == 1:
status['msg'] = subcode_entry[0].values()[0]
elif len(message_entry) == 1:
- status['msg'] = message_entry[0].text
+ status['msg'] = OneLogin_Saml2_Utils.element_text(message_entry[0])
return status
@@ -1125,7 +1130,7 @@ def validate_node_sign(signature_node, elem, cert=None, fingerprint=None, finger
x509_certificate_nodes = OneLogin_Saml2_Utils.query(signature_node, '//ds:Signature/ds:KeyInfo/ds:X509Data/ds:X509Certificate')
if len(x509_certificate_nodes) > 0:
x509_certificate_node = x509_certificate_nodes[0]
- x509_cert_value = x509_certificate_node.text
+ x509_cert_value = OneLogin_Saml2_Utils.element_text(x509_certificate_node)
x509_fingerprint_value = OneLogin_Saml2_Utils.calculate_x509_fingerprint(x509_cert_value, fingerprintalg)
if fingerprint == x509_fingerprint_value:
cert = OneLogin_Saml2_Utils.format_cert(x509_cert_value)
diff --git a/tests/data/responses/response_node_text_attack.xml.base64 b/tests/data/responses/response_node_text_attack.xml.base64
new file mode 100644
index 00000000..ba9f2f12
--- /dev/null
+++ b/tests/data/responses/response_node_text_attack.xml.base64
@@ -0,0 +1 @@
+PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIElEPSJHT1NBTUxSMTI5MDExNzQ1NzE3OTQiIFZlcnNpb249IjIuMCIgSXNzdWVJbnN0YW50PSIyMDEwLTExLTE4VDIxOjU3OjM3WiIgRGVzdGluYXRpb249IntyZWNpcGllbnR9Ij4NCiAgPHNhbWxwOlN0YXR1cz4NCiAgICA8c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+DQogIDxzYW1sOkFzc2VydGlvbiB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIFZlcnNpb249IjIuMCIgSUQ9InBmeGE0NjU3NGRmLWIzYjAtYTA2YS0yM2M4LTYzNjQxMzE5ODc3MiIgSXNzdWVJbnN0YW50PSIyMDEwLTExLTE4VDIxOjU3OjM3WiI+DQogICAgPHNhbWw6SXNzdWVyPmh0dHBzOi8vYXBwLm9uZWxvZ2luLmNvbS9zYW1sL21ldGFkYXRhLzEzNTkwPC9zYW1sOklzc3Vlcj4NCiAgICA8ZHM6U2lnbmF0dXJlIHhtbG5zOmRzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjIj4NCiAgICAgIDxkczpTaWduZWRJbmZvPg0KICAgICAgICA8ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPg0KICAgICAgICA8ZHM6U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3JzYS1zaGExIi8+DQogICAgICAgIDxkczpSZWZlcmVuY2UgVVJJPSIjcGZ4YTQ2NTc0ZGYtYjNiMC1hMDZhLTIzYzgtNjM2NDEzMTk4NzcyIj4NCiAgICAgICAgICA8ZHM6VHJhbnNmb3Jtcz4NCiAgICAgICAgICAgIDxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPg0KICAgICAgICAgICAgPGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPg0KICAgICAgICAgIDwvZHM6VHJhbnNmb3Jtcz4NCiAgICAgICAgICA8ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz4NCiAgICAgICAgICA8ZHM6RGlnZXN0VmFsdWU+cEpRN01TL2VrNEtSUldHbXYvSDQzUmVIWU1zPTwvZHM6RGlnZXN0VmFsdWU+DQogICAgICAgIDwvZHM6UmVmZXJlbmNlPg0KICAgICAgPC9kczpTaWduZWRJbmZvPg0KICAgICAgPGRzOlNpZ25hdHVyZVZhbHVlPnlpdmVLY1BkRHB1RE5qNnNoclEzQUJ3ci9jQTNDcnlEMnBoRy94TFpzektXeFU1L21sYUt0OGV3YlpPZEtLdnRPczJwSEJ5NUR1YTNrOTRBRnp4R3llbDVnT293bW95WEpyQU9ya1BPMHZsaTFWOG8zaFBQVVp3UmdTWDZROXBTMUNxUWdoS2lFYXNSeXlscXFKVWFQWXptT3pPRTgvWGxNa3dpV21PMD08L2RzOlNpZ25hdHVyZVZhbHVlPg0KICAgICAgPGRzOktleUluZm8+DQogICAgICAgIDxkczpYNTA5RGF0YT4NCiAgICAgICAgICA8ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUJyVENDQWFHZ0F3SUJBZ0lCQVRBREJnRUFNR2N4Q3pBSkJnTlZCQVlUQWxWVE1STXdFUVlEVlFRSURBcERZV3hwWm05eWJtbGhNUlV3RXdZRFZRUUhEQXhUWVc1MFlTQk5iMjVwWTJFeEVUQVBCZ05WQkFvTUNFOXVaVXh2WjJsdU1Sa3dGd1lEVlFRRERCQmhjSEF1YjI1bGJHOW5hVzR1WTI5dE1CNFhEVEV3TURNd09UQTVOVGcwTlZvWERURTFNRE13T1RBNU5UZzBOVm93WnpFTE1Ba0dBMVVFQmhNQ1ZWTXhFekFSQmdOVkJBZ01Da05oYkdsbWIzSnVhV0V4RlRBVEJnTlZCQWNNREZOaGJuUmhJRTF2Ym1sallURVJNQThHQTFVRUNnd0lUMjVsVEc5bmFXNHhHVEFYQmdOVkJBTU1FR0Z3Y0M1dmJtVnNiMmRwYmk1amIyMHdnWjh3RFFZSktvWklodmNOQVFFQkJRQURnWTBBTUlHSkFvR0JBT2pTdTFmalB5OGQ1dzRReUwxemQ0aEl3MU1ra2ZmNFdZL1RMRzhPWmtVNVlUU1dtbUhQRDVrdllINXVvWFMvNnFRODFxWHBSMndWOENUb3daSlVMZzA5ZGRSZFJuOFFzcWoxRnlPQzVzbEUzeTJiWjJvRnVhNzJvZi80OWZwdWpuRlQ2S25RNjFDQk1xbERvVFFxT1Q2MnZHSjhuUDZNWld2QTZzeHF1ZDVBZ01CQUFFd0F3WUJBQU1CQUE9PTwvZHM6WDUwOUNlcnRpZmljYXRlPg0KICAgICAgICA8L2RzOlg1MDlEYXRhPg0KICAgICAgPC9kczpLZXlJbmZvPg0KICAgIDwvZHM6U2lnbmF0dXJlPg0KICAgIDxzYW1sOlN1YmplY3Q+DQogICAgICA8c2FtbDpOYW1lSUQgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoxLjE6bmFtZWlkLWZvcm1hdDplbWFpbEFkZHJlc3MiPnN1cHBvcnQ8IS0tIGF0dGFjayEgLS0+QG9uZWxvZ2luLmNvbTwvc2FtbDpOYW1lSUQ+DQogICAgICA8c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+DQogICAgICAgIDxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjAxMC0xMS0xOFQyMjowMjozN1oiIFJlY2lwaWVudD0ie3JlY2lwaWVudH0iLz48L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj4NCiAgICA8L3NhbWw6U3ViamVjdD4NCiAgICA8c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxMC0xMS0xOFQyMTo1MjozN1oiIE5vdE9uT3JBZnRlcj0iMjAxMC0xMS0xOFQyMjowMjozN1oiPg0KICAgICAgPHNhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj4NCiAgICAgICAgPHNhbWw6QXVkaWVuY2U+e2F1ZGllbmNlfTwvc2FtbDpBdWRpZW5jZT4NCiAgICAgIDwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPg0KICAgIDwvc2FtbDpDb25kaXRpb25zPg0KICAgIDxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxMC0xMS0xOFQyMTo1NzozN1oiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwMTAtMTEtMTlUMjE6NTc6MzdaIiBTZXNzaW9uSW5kZXg9Il81MzFjMzJkMjgzYmRmZjdlMDRlNDg3YmNkYmM0ZGQ4ZCI+DQogICAgICA8c2FtbDpBdXRobkNvbnRleHQ+DQogICAgICAgIDxzYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkPC9zYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPg0KICAgICAgPC9zYW1sOkF1dGhuQ29udGV4dD4NCiAgICA8L3NhbWw6QXV0aG5TdGF0ZW1lbnQ+DQogICAgPHNhbWw6QXR0cmlidXRlU3RhdGVtZW50Pg0KICAgICAgPHNhbWw6QXR0cmlidXRlIE5hbWU9InN1cm5hbWUiPg0KICAgICAgICA8c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhzaTp0eXBlPSJ4czpzdHJpbmciPnM8IS0tIGF0dGFjayEgLS0+bWl0aDwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT4NCiAgICAgIDwvc2FtbDpBdHRyaWJ1dGU+DQogICAgICA8c2FtbDpBdHRyaWJ1dGUgTmFtZT0iYW5vdGhlcl92YWx1ZSI+DQogICAgICAgIDxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnR5cGU9InhzOnN0cmluZyI+dmFsdWUxPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPg0KICAgICAgICA8c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhzaTp0eXBlPSJ4czpzdHJpbmciPnZhbHVlMjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT4NCiAgICAgIDwvc2FtbDpBdHRyaWJ1dGU+DQogICAgICA8c2FtbDpBdHRyaWJ1dGUgTmFtZT0icm9sZSI+DQogICAgICAgIDxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnR5cGU9InhzOnN0cmluZyI+cm9sZTE8L3NhbWw6QXR0cmlidXRlVmFsdWU+DQogICAgICA8L3NhbWw6QXR0cmlidXRlPg0KICAgIDwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+DQogICAgPHNhbWw6QXR0cmlidXRlU3RhdGVtZW50Pg0KICAgICAgPHNhbWw6QXR0cmlidXRlIE5hbWU9ImZpcnN0bmFtZSI+DQogICAgICAgIDxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnR5cGU9InhzOnN0cmluZyI+Ym9iPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPg0KICAgICAgPC9zYW1sOkF0dHJpYnV0ZT4gIA0KICAgICAgPHNhbWw6QXR0cmlidXRlIE5hbWU9ImF0dHJpYnV0ZV93aXRoX25pbF92YWx1ZSI+DQogICAgICAgIDxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOm5pbD0idHJ1ZSIvPg0KICAgICAgPC9zYW1sOkF0dHJpYnV0ZT4NCiAgICAgIDxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJhdHRyaWJ1dGVfd2l0aF9uaWxzX2FuZF9lbXB0eV9zdHJpbmdzIj4NCiAgICAgICAgPHNhbWw6QXR0cmlidXRlVmFsdWUvPg0KICAgICAgICA8c2FtbDpBdHRyaWJ1dGVWYWx1ZT52YWx1ZVByZXNlbnQ8L3NhbWw6QXR0cmlidXRlVmFsdWU+DQogICAgICAgIDxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOm5pbD0idHJ1ZSIvPg0KICAgICAgICA8c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhzaTpuaWw9IjEiLz4NCiAgICAgIDwvc2FtbDpBdHRyaWJ1dGU+DQogICAgPC9zYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD4NCiAgPC9zYW1sOkFzc2VydGlvbj4NCjwvc2FtbHA6UmVzcG9uc2U+
\ No newline at end of file
diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py
index 8df0ddfd..44e9ead8 100644
--- a/tests/src/OneLogin/saml2_tests/response_test.py
+++ b/tests/src/OneLogin/saml2_tests/response_test.py
@@ -648,6 +648,19 @@ def testDoesNotAllowSignatureWrappingAttack(self):
self.assertFalse(response.is_valid(self.get_request_data()))
self.assertEqual('test@onelogin.com', response.get_nameid())
+ def testNodeTextAttack(self):
+ """
+ Tests the get_nameid and get_attributes methods of the OneLogin_Saml2_Response
+ Test that the node text with comment attack (VU#475445) is not allowed
+ """
+ settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
+ xml = self.file_contents(join(self.data_path, 'responses', 'response_node_text_attack.xml.base64'))
+ response = OneLogin_Saml2_Response(settings, xml)
+ nameid = response.get_nameid()
+ attributes = response.get_attributes()
+ self.assertEqual("smith", attributes.get('surname')[0])
+ self.assertEqual('support@onelogin.com', nameid)
+
def testGetSessionNotOnOrAfter(self):
"""
Tests the get_session_not_on_or_after method of the OneLogin_Saml2_Response
From 826f4f53812e87d953ed424fb972223c0edc4a45 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Tue, 27 Feb 2018 14:31:55 +0100
Subject: [PATCH 130/255] Improve how fingerprint is calcultated
---
src/onelogin/saml2/utils.py | 32 +++++++++++--------
.../src/OneLogin/saml2_tests/response_test.py | 2 +-
2 files changed, 20 insertions(+), 14 deletions(-)
diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py
index a9ece8ba..dafeaa73 100644
--- a/src/onelogin/saml2/utils.py
+++ b/src/onelogin/saml2/utils.py
@@ -557,9 +557,9 @@ def delete_local_session(callback=None):
@staticmethod
def calculate_x509_fingerprint(x509_cert, alg='sha1'):
"""
- Calculates the fingerprint of a x509cert.
+ Calculates the fingerprint of a formatted x509cert.
- :param x509_cert: x509 cert
+ :param x509_cert: x509 cert formatted
:type: string
:param alg: The algorithm to build the fingerprint
@@ -572,23 +572,27 @@ def calculate_x509_fingerprint(x509_cert, alg='sha1'):
lines = x509_cert.split('\n')
data = ''
+ inData = False
for line in lines:
# Remove '\r' from end of line if present.
line = line.rstrip()
- if line == '-----BEGIN CERTIFICATE-----':
- # Delete junk from before the certificate.
- data = ''
- elif line == '-----END CERTIFICATE-----':
- # Ignore data after the certificate.
- break
- elif line == '-----BEGIN PUBLIC KEY-----' or line == '-----BEGIN RSA PRIVATE KEY-----':
- # This isn't an X509 certificate.
- return None
+ if not inData:
+ if line == '-----BEGIN CERTIFICATE-----':
+ inData = True
+ elif line == '-----BEGIN PUBLIC KEY-----' or line == '-----BEGIN RSA PRIVATE KEY-----':
+ # This isn't an X509 certificate.
+ return None
else:
+ if line == '-----END CERTIFICATE-----':
+ break
+
# Append the current line to the certificate data.
data += line
+ if not data:
+ return None
+
decoded_data = base64.b64decode(data)
if alg == 'sha512':
@@ -1131,9 +1135,11 @@ def validate_node_sign(signature_node, elem, cert=None, fingerprint=None, finger
if len(x509_certificate_nodes) > 0:
x509_certificate_node = x509_certificate_nodes[0]
x509_cert_value = OneLogin_Saml2_Utils.element_text(x509_certificate_node)
- x509_fingerprint_value = OneLogin_Saml2_Utils.calculate_x509_fingerprint(x509_cert_value, fingerprintalg)
+ x509_cert_value_formatted = OneLogin_Saml2_Utils.format_cert(x509_cert_value)
+ x509_fingerprint_value = OneLogin_Saml2_Utils.calculate_x509_fingerprint(x509_cert_value_formatted, fingerprintalg)
+
if fingerprint == x509_fingerprint_value:
- cert = OneLogin_Saml2_Utils.format_cert(x509_cert_value)
+ cert = x509_cert_value_formatted
# Check if Reference URI is empty
# reference_elem = OneLogin_Saml2_Utils.query(signature_node, '//ds:Reference')
diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py
index 44e9ead8..e6793b87 100644
--- a/tests/src/OneLogin/saml2_tests/response_test.py
+++ b/tests/src/OneLogin/saml2_tests/response_test.py
@@ -1430,7 +1430,7 @@ def testIsValid2(self):
self.assertTrue(response_2.is_valid(self.get_request_data()))
settings_info_3 = self.loadSettingsJSON('settings2.json')
- idp_cert = settings_info_3['idp']['x509cert']
+ idp_cert = OneLogin_Saml2_Utils.format_cert(settings_info_3['idp']['x509cert'])
settings_info_3['idp']['certFingerprint'] = OneLogin_Saml2_Utils.calculate_x509_fingerprint(idp_cert)
settings_info_3['idp']['x509cert'] = ''
settings_3 = OneLogin_Saml2_Settings(settings_info_3)
From 27bce0081b180f90ac094ceefaa38183bfc47212 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Tue, 27 Feb 2018 16:30:15 +0100
Subject: [PATCH 131/255] Release 2.4.0
---
README.md | 4 ++++
changelog.md | 8 ++++++++
setup.py | 2 +-
3 files changed, 13 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 10ade02f..1399d32b 100644
--- a/README.md
+++ b/README.md
@@ -14,6 +14,10 @@ Python3: [python3-saml](https://github.com/onelogin/python3-saml).
#### Warning ####
+Update python-saml to 2.4.0, this version includes a fix for the [CVE-2017-11427](https://www.cvedetails.com/cve/CVE-2017-11427/) vulnerability.
+
+That version also change how calculate fingerprint method works, and will expect as input a formatted x509 certificate
+
Update python-saml to 2.2.3, this version replaces some etree.tostring calls, that were introduced recently, by the sanitized call provided by defusedxml
Update python-saml to 2.2.0, this version includes a security patch that contains extra validations that will prevent signature wrapping attacks. [CVE-2016-1000252](https://github.com/distributedweaknessfiling/DWF-Database-Artifacts/blob/master/DWF/2016/1000252/CVE-2016-1000252.json)
diff --git a/changelog.md b/changelog.md
index 7fc8e5a9..474a6549 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,4 +1,12 @@
# python-saml changelog
+### 2.4.0 (Feb 27, 2018)
+* Fix vulnerability [CVE-2017-11427](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-11427). Process text of nodes properly, ignoring comments
+* Improve how fingerprint is calcultated
+* Fix issue with LogoutRequest rejected by ADFS due NameID with unspecified format instead no format attribute
+* Be able to invalidate a SAMLResponse if it contains InResponseTo value but no RequestId parameter provided at the is_valid method. See rejectUnsolicitedResponsesWithInResponseTo security parameter (By default deactivated)
+* Fix signature position in the SP metadata
+* Redefine NSMAP constant
+
### 2.3.0 (Sep 15, 2017)
* [#205](https://github.com/onelogin/python-saml/pull/205) Improve decrypt method, Add an option to decrypt an element in place or copy it before decryption.
* [#204](https://github.com/onelogin/python-saml/pull/204) On a LogoutRequest if the NameIdFormat is entity, NameQualifier and SPNameQualifier will be ommited. If the NameIdFormat is not entity and a NameQualifier is provided, then the SPNameQualifier will be also added.
diff --git a/setup.py b/setup.py
index 39e1be11..ad531eb4 100644
--- a/setup.py
+++ b/setup.py
@@ -9,7 +9,7 @@
setup(
name='python-saml',
- version='2.3.0',
+ version='2.4.0',
description='Onelogin Python Toolkit. Add SAML support to your Python software using this library',
classifiers=[
'Development Status :: 5 - Production/Stable',
From 875dad4643743917b3258af049b03aacc500b1d3 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Tue, 27 Feb 2018 17:31:00 +0100
Subject: [PATCH 132/255] Update README.md
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 1399d32b..8d31d913 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,7 @@ Python3: [python3-saml](https://github.com/onelogin/python3-saml).
Update python-saml to 2.4.0, this version includes a fix for the [CVE-2017-11427](https://www.cvedetails.com/cve/CVE-2017-11427/) vulnerability.
-That version also change how calculate fingerprint method works, and will expect as input a formatted x509 certificate
+This version also changes how the calculate fingerprint method works, and will expect as input a formatted x509 certificate
Update python-saml to 2.2.3, this version replaces some etree.tostring calls, that were introduced recently, by the sanitized call provided by defusedxml
From 7ab94910ecc359e545417abecb1d10c52e8ee040 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Thu, 5 Apr 2018 17:03:44 +0200
Subject: [PATCH 133/255] Update defusedxml dependency.
---
setup.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/setup.py b/setup.py
index ad531eb4..10d676b8 100644
--- a/setup.py
+++ b/setup.py
@@ -34,7 +34,7 @@
install_requires=[
'dm.xmlsec.binding==1.3.3',
'isodate>=0.5.0',
- 'defusedxml==0.4.1',
+ 'defusedxml>=0.4.1',
],
extras_require={
'test': (
From 2b283b5568e67d8139f46ad8e7426d4f37172d86 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Mon, 23 Apr 2018 18:26:28 +0200
Subject: [PATCH 134/255] Update copyright and license reference
---
LICENSE | 2 +-
docs/saml2/_modules/saml2/auth.html | 4 ++--
docs/saml2/_modules/saml2/authn_request.html | 4 ++--
docs/saml2/_modules/saml2/constants.html | 4 ++--
docs/saml2/_modules/saml2/errors.html | 4 ++--
docs/saml2/_modules/saml2/logout_request.html | 4 ++--
docs/saml2/_modules/saml2/logout_response.html | 4 ++--
docs/saml2/_modules/saml2/metadata.html | 4 ++--
docs/saml2/_modules/saml2/response.html | 4 ++--
docs/saml2/_modules/saml2/settings.html | 4 ++--
docs/saml2/_modules/saml2/utils.html | 4 ++--
setup.py | 4 ++--
src/onelogin/__init__.py | 4 ++--
src/onelogin/saml2/__init__.py | 4 ++--
src/onelogin/saml2/auth.py | 4 ++--
src/onelogin/saml2/authn_request.py | 4 ++--
src/onelogin/saml2/constants.py | 4 ++--
src/onelogin/saml2/errors.py | 4 ++--
src/onelogin/saml2/idp_metadata_parser.py | 4 ++--
src/onelogin/saml2/logout_request.py | 4 ++--
src/onelogin/saml2/logout_response.py | 4 ++--
src/onelogin/saml2/metadata.py | 4 ++--
src/onelogin/saml2/response.py | 4 ++--
src/onelogin/saml2/settings.py | 4 ++--
src/onelogin/saml2/utils.py | 4 ++--
tests/src/OneLogin/saml2_tests/auth_test.py | 4 ++--
tests/src/OneLogin/saml2_tests/authn_request_test.py | 4 ++--
tests/src/OneLogin/saml2_tests/error_test.py | 4 ++--
tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py | 4 ++--
tests/src/OneLogin/saml2_tests/logout_request_test.py | 4 ++--
tests/src/OneLogin/saml2_tests/logout_response_test.py | 4 ++--
tests/src/OneLogin/saml2_tests/metadata_test.py | 4 ++--
tests/src/OneLogin/saml2_tests/response_test.py | 4 ++--
tests/src/OneLogin/saml2_tests/settings_test.py | 4 ++--
tests/src/OneLogin/saml2_tests/signed_response_test.py | 4 ++--
tests/src/OneLogin/saml2_tests/utils_test.py | 4 ++--
36 files changed, 71 insertions(+), 71 deletions(-)
diff --git a/LICENSE b/LICENSE
index dbbca9c6..1c8f814e 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2010-2016 OneLogin, Inc.
+Copyright (c) 2010-2018 OneLogin, Inc.
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
diff --git a/docs/saml2/_modules/saml2/auth.html b/docs/saml2/_modules/saml2/auth.html
index 82d6e960..243bd4db 100644
--- a/docs/saml2/_modules/saml2/auth.html
+++ b/docs/saml2/_modules/saml2/auth.html
@@ -51,8 +51,8 @@ Navigation
Source code for saml2.auth
# -*- coding: utf-8 -*-
-# Copyright (c) 2014, OneLogin, Inc.
-# All rights reserved.
+# Copyright (c) 2010-2018 OneLogin, Inc.
+# MIT License
from base64 import b64encode
from urllib import urlencode , quote
diff --git a/docs/saml2/_modules/saml2/authn_request.html b/docs/saml2/_modules/saml2/authn_request.html
index 674eae42..207ee7a6 100644
--- a/docs/saml2/_modules/saml2/authn_request.html
+++ b/docs/saml2/_modules/saml2/authn_request.html
@@ -51,8 +51,8 @@ Navigation
Source code for saml2.authn_request
# -*- coding: utf-8 -*-
-# Copyright (c) 2014, OneLogin, Inc.
-# All rights reserved.
+# Copyright (c) 2010-2018 OneLogin, Inc.
+# MIT License
from base64 import b64encode
from datetime import datetime
diff --git a/docs/saml2/_modules/saml2/constants.html b/docs/saml2/_modules/saml2/constants.html
index 3b098931..306ec1b3 100644
--- a/docs/saml2/_modules/saml2/constants.html
+++ b/docs/saml2/_modules/saml2/constants.html
@@ -51,8 +51,8 @@ Navigation
Source code for saml2.constants
# -*- coding: utf-8 -*-
-# Copyright (c) 2014, OneLogin, Inc.
-# All rights reserved.
+# Copyright (c) 2010-2018 OneLogin, Inc.
+# MIT License
[docs] class OneLogin_Saml2_Constants :
diff --git a/docs/saml2/_modules/saml2/errors.html b/docs/saml2/_modules/saml2/errors.html
index e33ebb39..ef958f31 100644
--- a/docs/saml2/_modules/saml2/errors.html
+++ b/docs/saml2/_modules/saml2/errors.html
@@ -51,8 +51,8 @@
Navigation
Source code for saml2.errors
# -*- coding: utf-8 -*-
-# Copyright (c) 2014, OneLogin, Inc.
-# All rights reserved.
+# Copyright (c) 2010-2018 OneLogin, Inc.
+# MIT License
[docs] class OneLogin_Saml2_Error ( Exception ):
diff --git a/docs/saml2/_modules/saml2/logout_request.html b/docs/saml2/_modules/saml2/logout_request.html
index b6cddbc1..1b690dd4 100644
--- a/docs/saml2/_modules/saml2/logout_request.html
+++ b/docs/saml2/_modules/saml2/logout_request.html
@@ -51,8 +51,8 @@
Navigation
Source code for saml2.logout_request
# -*- coding: utf-8 -*-
-# Copyright (c) 2014, OneLogin, Inc.
-# All rights reserved.
+# Copyright (c) 2010-2018 OneLogin, Inc.
+# MIT License
from base64 import b64decode
from datetime import datetime
diff --git a/docs/saml2/_modules/saml2/logout_response.html b/docs/saml2/_modules/saml2/logout_response.html
index dc6037d1..3ea3bbf7 100644
--- a/docs/saml2/_modules/saml2/logout_response.html
+++ b/docs/saml2/_modules/saml2/logout_response.html
@@ -51,8 +51,8 @@ Navigation
Source code for saml2.logout_response
# -*- coding: utf-8 -*-
-# Copyright (c) 2014, OneLogin, Inc.
-# All rights reserved.
+# Copyright (c) 2010-2018 OneLogin, Inc.
+# MIT License
from base64 import b64decode
from datetime import datetime
diff --git a/docs/saml2/_modules/saml2/metadata.html b/docs/saml2/_modules/saml2/metadata.html
index 055db317..7b0a1be5 100644
--- a/docs/saml2/_modules/saml2/metadata.html
+++ b/docs/saml2/_modules/saml2/metadata.html
@@ -51,8 +51,8 @@ Navigation
Source code for saml2.metadata
# -*- coding: utf-8 -*-
-# Copyright (c) 2014, OneLogin, Inc.
-# All rights reserved.
+# Copyright (c) 2010-2018 OneLogin, Inc.
+# MIT License
from time import gmtime , strftime
from datetime import datetime
diff --git a/docs/saml2/_modules/saml2/response.html b/docs/saml2/_modules/saml2/response.html
index 219be1b8..d3fee4e3 100644
--- a/docs/saml2/_modules/saml2/response.html
+++ b/docs/saml2/_modules/saml2/response.html
@@ -51,8 +51,8 @@ Navigation
Source code for saml2.response
# -*- coding: utf-8 -*-
-# Copyright (c) 2014, OneLogin, Inc.
-# All rights reserved.
+# Copyright (c) 2010-2018 OneLogin, Inc.
+# MIT License
from base64 import b64decode
from copy import deepcopy
diff --git a/docs/saml2/_modules/saml2/settings.html b/docs/saml2/_modules/saml2/settings.html
index c18ebdf9..42800b51 100644
--- a/docs/saml2/_modules/saml2/settings.html
+++ b/docs/saml2/_modules/saml2/settings.html
@@ -51,8 +51,8 @@ Navigation
Source code for saml2.settings
# -*- coding: utf-8 -*-
-# Copyright (c) 2014, OneLogin, Inc.
-# All rights reserved.
+# Copyright (c) 2010-2018 OneLogin, Inc.
+# MIT License
from datetime import datetime
import json
diff --git a/docs/saml2/_modules/saml2/utils.html b/docs/saml2/_modules/saml2/utils.html
index 726ba9dd..613d2a09 100644
--- a/docs/saml2/_modules/saml2/utils.html
+++ b/docs/saml2/_modules/saml2/utils.html
@@ -51,8 +51,8 @@ Navigation
Source code for saml2.utils
# -*- coding: utf-8 -*-
-# Copyright (c) 2014, OneLogin, Inc.
-# All rights reserved.
+# Copyright (c) 2010-2018 OneLogin, Inc.
+# MIT License
import base64
from datetime import datetime
diff --git a/setup.py b/setup.py
index 10d676b8..9b5cb63e 100644
--- a/setup.py
+++ b/setup.py
@@ -1,8 +1,8 @@
#! /usr/bin/env python
# -*- coding: utf-8 -*-
-# Copyright (c) 2014, OneLogin, Inc.
-# All rights reserved.
+# Copyright (c) 2010-2018 OneLogin, Inc.
+# MIT License
from setuptools import setup
diff --git a/src/onelogin/__init__.py b/src/onelogin/__init__.py
index 110bc9df..ba664a65 100644
--- a/src/onelogin/__init__.py
+++ b/src/onelogin/__init__.py
@@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
"""
-Copyright (c) 2014, OneLogin, Inc.
-All rights reserved.
+Copyright (c) 2010-2018 OneLogin, Inc.
+MIT License
Add SAML support to your Python softwares using this library.
Forget those complicated libraries and use that open source
diff --git a/src/onelogin/saml2/__init__.py b/src/onelogin/saml2/__init__.py
index 110bc9df..ba664a65 100644
--- a/src/onelogin/saml2/__init__.py
+++ b/src/onelogin/saml2/__init__.py
@@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
"""
-Copyright (c) 2014, OneLogin, Inc.
-All rights reserved.
+Copyright (c) 2010-2018 OneLogin, Inc.
+MIT License
Add SAML support to your Python softwares using this library.
Forget those complicated libraries and use that open source
diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py
index bb564e4d..8022ca24 100644
--- a/src/onelogin/saml2/auth.py
+++ b/src/onelogin/saml2/auth.py
@@ -2,8 +2,8 @@
""" OneLogin_Saml2_Auth class
-Copyright (c) 2014, OneLogin, Inc.
-All rights reserved.
+Copyright (c) 2010-2018 OneLogin, Inc.
+MIT License
Main class of OneLogin's Python Toolkit.
diff --git a/src/onelogin/saml2/authn_request.py b/src/onelogin/saml2/authn_request.py
index 636a6888..d0983703 100644
--- a/src/onelogin/saml2/authn_request.py
+++ b/src/onelogin/saml2/authn_request.py
@@ -2,8 +2,8 @@
""" OneLogin_Saml2_Authn_Request class
-Copyright (c) 2014, OneLogin, Inc.
-All rights reserved.
+Copyright (c) 2010-2018 OneLogin, Inc.
+MIT License
AuthNRequest class of OneLogin's Python Toolkit.
diff --git a/src/onelogin/saml2/constants.py b/src/onelogin/saml2/constants.py
index 2bf885ef..895c248a 100644
--- a/src/onelogin/saml2/constants.py
+++ b/src/onelogin/saml2/constants.py
@@ -2,8 +2,8 @@
""" OneLogin_Saml2_Constants class
-Copyright (c) 2014, OneLogin, Inc.
-All rights reserved.
+Copyright (c) 2010-2018 OneLogin, Inc.
+MIT License
Constants class of OneLogin's Python Toolkit.
diff --git a/src/onelogin/saml2/errors.py b/src/onelogin/saml2/errors.py
index 0675fb4a..ec879bfd 100644
--- a/src/onelogin/saml2/errors.py
+++ b/src/onelogin/saml2/errors.py
@@ -2,8 +2,8 @@
""" OneLogin_Saml2_Error class
-Copyright (c) 2014, OneLogin, Inc.
-All rights reserved.
+Copyright (c) 2010-2018 OneLogin, Inc.
+MIT License
Error class of OneLogin's Python Toolkit.
diff --git a/src/onelogin/saml2/idp_metadata_parser.py b/src/onelogin/saml2/idp_metadata_parser.py
index 51d446ff..3b5637a5 100644
--- a/src/onelogin/saml2/idp_metadata_parser.py
+++ b/src/onelogin/saml2/idp_metadata_parser.py
@@ -2,8 +2,8 @@
""" OneLogin_Saml2_IdPMetadataParser class
-Copyright (c) 2014, OneLogin, Inc.
-All rights reserved.
+Copyright (c) 2010-2018 OneLogin, Inc.
+MIT License
Metadata class of OneLogin's Python Toolkit.
diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py
index 14c49e29..5945c652 100644
--- a/src/onelogin/saml2/logout_request.py
+++ b/src/onelogin/saml2/logout_request.py
@@ -2,8 +2,8 @@
""" OneLogin_Saml2_Logout_Request class
-Copyright (c) 2014, OneLogin, Inc.
-All rights reserved.
+Copyright (c) 2010-2018 OneLogin, Inc.
+MIT License
Logout Request class of OneLogin's Python Toolkit.
diff --git a/src/onelogin/saml2/logout_response.py b/src/onelogin/saml2/logout_response.py
index 0cd1e2be..75ade5e8 100644
--- a/src/onelogin/saml2/logout_response.py
+++ b/src/onelogin/saml2/logout_response.py
@@ -2,8 +2,8 @@
""" OneLogin_Saml2_Logout_Response class
-Copyright (c) 2014, OneLogin, Inc.
-All rights reserved.
+Copyright (c) 2010-2018 OneLogin, Inc.
+MIT License
Logout Response class of OneLogin's Python Toolkit.
diff --git a/src/onelogin/saml2/metadata.py b/src/onelogin/saml2/metadata.py
index c431be48..212c0e95 100644
--- a/src/onelogin/saml2/metadata.py
+++ b/src/onelogin/saml2/metadata.py
@@ -2,8 +2,8 @@
""" OneLogin_Saml2_Metadata class
-Copyright (c) 2014, OneLogin, Inc.
-All rights reserved.
+Copyright (c) 2010-2018 OneLogin, Inc.
+MIT License
Metadata class of OneLogin's Python Toolkit.
diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py
index 56a8476b..b9eb3f8e 100644
--- a/src/onelogin/saml2/response.py
+++ b/src/onelogin/saml2/response.py
@@ -2,8 +2,8 @@
""" OneLogin_Saml2_Response class
-Copyright (c) 2014, OneLogin, Inc.
-All rights reserved.
+Copyright (c) 2010-2018 OneLogin, Inc.
+MIT License
SAML Response class of OneLogin's Python Toolkit.
diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py
index d5db3a05..f3c53be1 100644
--- a/src/onelogin/saml2/settings.py
+++ b/src/onelogin/saml2/settings.py
@@ -2,8 +2,8 @@
""" OneLogin_Saml2_Settings class
-Copyright (c) 2014, OneLogin, Inc.
-All rights reserved.
+Copyright (c) 2010-2018 OneLogin, Inc.
+MIT License
Setting class of OneLogin's Python Toolkit.
diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py
index dafeaa73..5b1d9b8b 100644
--- a/src/onelogin/saml2/utils.py
+++ b/src/onelogin/saml2/utils.py
@@ -2,8 +2,8 @@
""" OneLogin_Saml2_Utils class
-Copyright (c) 2014, OneLogin, Inc.
-All rights reserved.
+Copyright (c) 2010-2018 OneLogin, Inc.
+MIT License
Auxiliary class of OneLogin's Python Toolkit.
diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py
index c50fc6e8..ee649448 100644
--- a/tests/src/OneLogin/saml2_tests/auth_test.py
+++ b/tests/src/OneLogin/saml2_tests/auth_test.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
-# Copyright (c) 2014, OneLogin, Inc.
-# All rights reserved.
+# Copyright (c) 2010-2018 OneLogin, Inc.
+# MIT License
from base64 import b64decode, b64encode
import json
diff --git a/tests/src/OneLogin/saml2_tests/authn_request_test.py b/tests/src/OneLogin/saml2_tests/authn_request_test.py
index 6aee012d..be891cff 100644
--- a/tests/src/OneLogin/saml2_tests/authn_request_test.py
+++ b/tests/src/OneLogin/saml2_tests/authn_request_test.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
-# Copyright (c) 2014, OneLogin, Inc.
-# All rights reserved.
+# Copyright (c) 2010-2018 OneLogin, Inc.
+# MIT License
from base64 import b64decode
import json
diff --git a/tests/src/OneLogin/saml2_tests/error_test.py b/tests/src/OneLogin/saml2_tests/error_test.py
index 7699300a..cd997cea 100644
--- a/tests/src/OneLogin/saml2_tests/error_test.py
+++ b/tests/src/OneLogin/saml2_tests/error_test.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
-# Copyright (c) 2014, OneLogin, Inc.
-# All rights reserved.
+# Copyright (c) 2010-2018 OneLogin, Inc.
+# MIT License
import unittest
from teamcity import is_running_under_teamcity
diff --git a/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py b/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py
index e441e0f0..4e0e79c2 100644
--- a/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py
+++ b/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
-# Copyright (c) 2014, OneLogin, Inc.
-# All rights reserved.
+# Copyright (c) 2010-2018 OneLogin, Inc.
+# MIT License
from copy import deepcopy
diff --git a/tests/src/OneLogin/saml2_tests/logout_request_test.py b/tests/src/OneLogin/saml2_tests/logout_request_test.py
index 6d78d1bc..3ecf901a 100644
--- a/tests/src/OneLogin/saml2_tests/logout_request_test.py
+++ b/tests/src/OneLogin/saml2_tests/logout_request_test.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
-# Copyright (c) 2014, OneLogin, Inc.
-# All rights reserved.
+# Copyright (c) 2010-2018 OneLogin, Inc.
+# MIT License
from base64 import b64encode
import json
diff --git a/tests/src/OneLogin/saml2_tests/logout_response_test.py b/tests/src/OneLogin/saml2_tests/logout_response_test.py
index f54690ed..27151a5a 100644
--- a/tests/src/OneLogin/saml2_tests/logout_response_test.py
+++ b/tests/src/OneLogin/saml2_tests/logout_response_test.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
-# Copyright (c) 2014, OneLogin, Inc.
-# All rights reserved.
+# Copyright (c) 2010-2018 OneLogin, Inc.
+# MIT License
import json
from os.path import dirname, join, exists
diff --git a/tests/src/OneLogin/saml2_tests/metadata_test.py b/tests/src/OneLogin/saml2_tests/metadata_test.py
index 02e551e7..70590497 100644
--- a/tests/src/OneLogin/saml2_tests/metadata_test.py
+++ b/tests/src/OneLogin/saml2_tests/metadata_test.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
-# Copyright (c) 2014, OneLogin, Inc.
-# All rights reserved.
+# Copyright (c) 2010-2018 OneLogin, Inc.
+# MIT License
import json
diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py
index e6793b87..a67c8f62 100644
--- a/tests/src/OneLogin/saml2_tests/response_test.py
+++ b/tests/src/OneLogin/saml2_tests/response_test.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
-# Copyright (c) 2014, OneLogin, Inc.
-# All rights reserved.
+# Copyright (c) 2010-2018 OneLogin, Inc.
+# MIT License
from base64 import b64decode, b64encode
from datetime import datetime
diff --git a/tests/src/OneLogin/saml2_tests/settings_test.py b/tests/src/OneLogin/saml2_tests/settings_test.py
index bcdd7e1f..4b79715d 100644
--- a/tests/src/OneLogin/saml2_tests/settings_test.py
+++ b/tests/src/OneLogin/saml2_tests/settings_test.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
-# Copyright (c) 2014, OneLogin, Inc.
-# All rights reserved.
+# Copyright (c) 2010-2018 OneLogin, Inc.
+# MIT License
import json
from os.path import dirname, join, exists, sep
diff --git a/tests/src/OneLogin/saml2_tests/signed_response_test.py b/tests/src/OneLogin/saml2_tests/signed_response_test.py
index d630f00f..12db9143 100644
--- a/tests/src/OneLogin/saml2_tests/signed_response_test.py
+++ b/tests/src/OneLogin/saml2_tests/signed_response_test.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
-# Copyright (c) 2014, OneLogin, Inc.
-# All rights reserved.
+# Copyright (c) 2010-2018 OneLogin, Inc.
+# MIT License
from base64 import b64encode
import json
diff --git a/tests/src/OneLogin/saml2_tests/utils_test.py b/tests/src/OneLogin/saml2_tests/utils_test.py
index 0b9772f2..916c9c81 100644
--- a/tests/src/OneLogin/saml2_tests/utils_test.py
+++ b/tests/src/OneLogin/saml2_tests/utils_test.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
-# Copyright (c) 2014, OneLogin, Inc.
-# All rights reserved.
+# Copyright (c) 2010-2018 OneLogin, Inc.
+# MIT License
from base64 import b64decode
import json
From 5e08bb24b5dc5ec1d76023c8620ce8228c588da9 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Mon, 23 Apr 2018 19:15:39 +0200
Subject: [PATCH 135/255] Update coveralls and coverage dependency version
---
setup.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/setup.py b/setup.py
index 9b5cb63e..1442b9f2 100644
--- a/setup.py
+++ b/setup.py
@@ -39,12 +39,12 @@
extras_require={
'test': (
'teamcity-messages==1.17',
- 'coverage==4.0.3',
+ 'coverage>=3.6',
'freezegun==0.3.5',
'pylint==1.3.1',
'pep8==1.5.7',
'pyflakes==0.8.1',
- 'coveralls==0.4.4',
+ 'coveralls==1.1',
),
},
keywords='saml saml2 xmlsec django flask',
From 562ad0dac3b2dc768b96105ba978e5f5eee60e5b Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Wed, 25 Apr 2018 15:48:25 +0200
Subject: [PATCH 136/255] Add ID to EntityDescriptor before sign it on add_sign
method. Improve the way ds namespace is handled in add_sign method
---
src/onelogin/saml2/utils.py | 33 ++++++++++---------
.../src/OneLogin/saml2_tests/metadata_test.py | 5 +++
2 files changed, 23 insertions(+), 15 deletions(-)
diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py
index 5b1d9b8b..4ae37efb 100644
--- a/src/onelogin/saml2/utils.py
+++ b/src/onelogin/saml2/utils.py
@@ -869,7 +869,6 @@ def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constant
error_callback_method = print_xmlsec_errors
xmlsec.set_error_callback(error_callback_method)
- # Sign the metadata with our private key.
sign_algorithm_transform_map = {
OneLogin_Saml2_Constants.DSA_SHA1: xmlsec.TransformDsaSha1,
OneLogin_Saml2_Constants.RSA_SHA1: xmlsec.TransformRsaSha1,
@@ -879,18 +878,31 @@ def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constant
}
sign_algorithm_transform = sign_algorithm_transform_map.get(sign_algorithm, xmlsec.TransformRsaSha1)
- signature = Signature(xmlsec.TransformExclC14N, sign_algorithm_transform)
+ signature = Signature(xmlsec.TransformExclC14N, sign_algorithm_transform, nsPrefix='ds')
issuer = OneLogin_Saml2_Utils.query(elem, '//saml:Issuer')
if len(issuer) > 0:
issuer = issuer[0]
issuer.addnext(signature)
+ elem_to_sign = issuer.getparent()
else:
entity_descriptor = OneLogin_Saml2_Utils.query(elem, '//md:EntityDescriptor')
if len(entity_descriptor) > 0:
elem.insert(0, signature)
else:
elem[0].insert(0, signature)
+ elem_to_sign = elem
+
+ elem_id = elem_to_sign.get('ID', None)
+ if elem_id is not None:
+ if elem_id:
+ elem_id = '#' + elem_id
+ else:
+ generated_id = generated_id = OneLogin_Saml2_Utils.generate_unique_id()
+ elem_id = '#' + generated_id
+ elem_to_sign.attrib['ID'] = generated_id
+
+ xmlsec.addIDs(elem_to_sign, ["ID"])
digest_algorithm_transform_map = {
OneLogin_Saml2_Constants.SHA1: xmlsec.TransformSha1,
@@ -901,6 +913,9 @@ def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constant
digest_algorithm_transform = digest_algorithm_transform_map.get(digest_algorithm, xmlsec.TransformSha1)
ref = signature.addReference(digest_algorithm_transform)
+ if elem_id:
+ ref.attrib['URI'] = elem_id
+
ref.addTransform(xmlsec.TransformEnveloped)
ref.addTransform(xmlsec.TransformExclC14N)
@@ -917,20 +932,8 @@ def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constant
dsig_ctx.signKey = sign_key
dsig_ctx.sign(signature)
+ return tostring(elem, encoding='unicode').encode('utf-8')
newdoc = parseString(tostring(elem, encoding='unicode').encode('utf-8'))
-
- signature_nodes = newdoc.getElementsByTagName("Signature")
-
- for signature in signature_nodes:
- signature.removeAttribute('xmlns')
- signature.setAttribute('xmlns:ds', OneLogin_Saml2_Constants.NS_DS)
- if not signature.tagName.startswith('ds:'):
- signature.tagName = 'ds:' + signature.tagName
- nodes = signature.getElementsByTagName("*")
- for node in nodes:
- if not node.tagName.startswith('ds:'):
- node.tagName = 'ds:' + node.tagName
-
return newdoc.saveXML(newdoc.firstChild)
@staticmethod
diff --git a/tests/src/OneLogin/saml2_tests/metadata_test.py b/tests/src/OneLogin/saml2_tests/metadata_test.py
index 70590497..f69f5c7e 100644
--- a/tests/src/OneLogin/saml2_tests/metadata_test.py
+++ b/tests/src/OneLogin/saml2_tests/metadata_test.py
@@ -14,6 +14,7 @@
from onelogin.saml2.metadata import OneLogin_Saml2_Metadata
from onelogin.saml2.settings import OneLogin_Saml2_Settings
+from onelogin.saml2.utils import OneLogin_Saml2_Utils
from onelogin.saml2.constants import OneLogin_Saml2_Constants
@@ -208,9 +209,11 @@ def testSignMetadata(self):
cert = self.file_contents(join(cert_path, 'sp.crt'))
signed_metadata = OneLogin_Saml2_Metadata.sign_metadata(metadata, key, cert)
+ self.assertTrue(OneLogin_Saml2_Utils.validate_metadata_sign(signed_metadata, cert))
self.assertIn('
Date: Wed, 25 Apr 2018 15:48:38 +0200
Subject: [PATCH 137/255] Release 2.4.1
---
changelog.md | 5 +++++
setup.py | 2 +-
2 files changed, 6 insertions(+), 1 deletion(-)
diff --git a/changelog.md b/changelog.md
index 474a6549..d17975f1 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,4 +1,9 @@
# python-saml changelog
+### 2.4.1 (Apr 25, 2018)
+* Add ID to EntityDescriptor before sign it on add_sign method. Improve the way ds namespace is handled in add_sign method
+* Update defusedxml, coveralls and coverage dependencies
+* Update copyright and license reference
+
### 2.4.0 (Feb 27, 2018)
* Fix vulnerability [CVE-2017-11427](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-11427). Process text of nodes properly, ignoring comments
* Improve how fingerprint is calcultated
diff --git a/setup.py b/setup.py
index 1442b9f2..e5598ec3 100644
--- a/setup.py
+++ b/setup.py
@@ -9,7 +9,7 @@
setup(
name='python-saml',
- version='2.4.0',
+ version='2.4.1',
description='Onelogin Python Toolkit. Add SAML support to your Python software using this library',
classifiers=[
'Development Status :: 5 - Production/Stable',
From 310fe65a8f4fe87c5be5d6357274502a2bd68487 Mon Sep 17 00:00:00 2001
From: afrobeard
Date: Tue, 10 Jul 2018 12:41:23 -0400
Subject: [PATCH 138/255] Making this module compatible with the Current LTS
version of Django
---
demo-django/demo/settings.py | 17 ++++++++++++-----
demo-django/demo/urls.py | 17 +++++++++--------
demo-django/demo/views.py | 19 ++++++-------------
demo-django/requirements.txt | 2 +-
4 files changed, 28 insertions(+), 27 deletions(-)
diff --git a/demo-django/demo/settings.py b/demo-django/demo/settings.py
index 3428fdab..792ea82f 100644
--- a/demo-django/demo/settings.py
+++ b/demo-django/demo/settings.py
@@ -22,8 +22,6 @@
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
-TEMPLATE_DEBUG = True
-
ALLOWED_HOSTS = []
@@ -85,6 +83,15 @@
SESSION_ENGINE = 'django.contrib.sessions.backends.file'
-TEMPLATE_DIRS = (
- os.path.join(BASE_DIR, 'templates'),
-)
+TEMPLATES = [
+ {
+ 'BACKEND': 'django.template.backends.django.DjangoTemplates',
+ 'DIRS': [os.path.join(BASE_DIR, 'templates')],
+ 'APP_DIRS': True,
+ 'OPTIONS': {
+ 'context_processors': {
+ 'django.contrib.auth.context_processors.auth'
+ }
+ },
+ },
+]
diff --git a/demo-django/demo/urls.py b/demo-django/demo/urls.py
index 4f55a0b3..1f329074 100644
--- a/demo-django/demo/urls.py
+++ b/demo-django/demo/urls.py
@@ -1,11 +1,12 @@
-from django.conf.urls import patterns, url
-
+from django.conf.urls import url
from django.contrib import admin
+from demo.views import index, attrs, metadata
+
admin.autodiscover()
-urlpatterns = patterns(
- '',
- url(r'^$', 'demo.views.index', name='index'),
- url(r'^attrs/$', 'demo.views.attrs', name='attrs'),
- url(r'^metadata/$', 'demo.views.metadata', name='metadata'),
-)
+urlpatterns = [
+ url(r'^$', index, name='index'),
+ url(r'^attrs/$', attrs, name='attrs'),
+ url(r'^metadata/$', metadata, name='metadata')
+]
+
diff --git a/demo-django/demo/views.py b/demo-django/demo/views.py
index 859ff9df..59ad2a05 100644
--- a/demo-django/demo/views.py
+++ b/demo-django/demo/views.py
@@ -2,8 +2,7 @@
from django.core.urlresolvers import reverse
from django.http import (HttpResponse, HttpResponseRedirect,
HttpResponseServerError)
-from django.shortcuts import render_to_response
-from django.template import RequestContext
+from django.shortcuts import render
from onelogin.saml2.auth import OneLogin_Saml2_Auth
from onelogin.saml2.settings import OneLogin_Saml2_Settings
@@ -97,13 +96,8 @@ def index(request):
if len(request.session['samlUserdata']) > 0:
attributes = request.session['samlUserdata'].items()
- return render_to_response('index.html',
- {'errors': errors,
- 'not_auth_warn': not_auth_warn,
- 'success_slo': success_slo,
- 'attributes': attributes,
- 'paint_logout': paint_logout},
- context_instance=RequestContext(request))
+ return render(request, 'index.html', {'errors': errors, 'not_auth_warn': not_auth_warn, 'success_slo': success_slo,
+ 'attributes': attributes, 'paint_logout': paint_logout})
def attrs(request):
@@ -115,10 +109,9 @@ def attrs(request):
if len(request.session['samlUserdata']) > 0:
attributes = request.session['samlUserdata'].items()
- return render_to_response('attrs.html',
- {'paint_logout': paint_logout,
- 'attributes': attributes},
- context_instance=RequestContext(request))
+ return render(request, 'attrs.html',
+ {'paint_logout': paint_logout,
+ 'attributes': attributes})
def metadata(request):
diff --git a/demo-django/requirements.txt b/demo-django/requirements.txt
index 5a55855e..f305b2f2 100644
--- a/demo-django/requirements.txt
+++ b/demo-django/requirements.txt
@@ -1 +1 @@
-Django==1.6.5
+Django==1.11
From b463dd4abc4087bc891ce8c4e2598bd20db464fb Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Thu, 26 Jul 2018 14:06:18 +0200
Subject: [PATCH 139/255] Remove TeamCity CI depedency
---
setup.py | 1 -
tests/src/OneLogin/saml2_tests/auth_test.py | 7 +------
tests/src/OneLogin/saml2_tests/authn_request_test.py | 7 +------
tests/src/OneLogin/saml2_tests/error_test.py | 7 +------
tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py | 7 +------
tests/src/OneLogin/saml2_tests/logout_request_test.py | 7 +------
tests/src/OneLogin/saml2_tests/logout_response_test.py | 7 +------
tests/src/OneLogin/saml2_tests/metadata_test.py | 7 +------
tests/src/OneLogin/saml2_tests/response_test.py | 7 +------
tests/src/OneLogin/saml2_tests/settings_test.py | 7 +------
tests/src/OneLogin/saml2_tests/signed_response_test.py | 7 +------
tests/src/OneLogin/saml2_tests/utils_test.py | 7 +------
12 files changed, 11 insertions(+), 67 deletions(-)
diff --git a/setup.py b/setup.py
index e5598ec3..1c23c817 100644
--- a/setup.py
+++ b/setup.py
@@ -38,7 +38,6 @@
],
extras_require={
'test': (
- 'teamcity-messages==1.17',
'coverage>=3.6',
'freezegun==0.3.5',
'pylint==1.3.1',
diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py
index ee649448..5bc13000 100644
--- a/tests/src/OneLogin/saml2_tests/auth_test.py
+++ b/tests/src/OneLogin/saml2_tests/auth_test.py
@@ -7,8 +7,6 @@
import json
from os.path import dirname, join, exists
import unittest
-from teamcity import is_running_under_teamcity
-from teamcity.unittestpy import TeamcityTestRunner
from urlparse import urlparse, parse_qs
from onelogin.saml2.auth import OneLogin_Saml2_Auth
@@ -1166,8 +1164,5 @@ def testGetIdFromLogoutResponse(self):
self.assertIn(auth.get_last_message_id(), '_f9ee61bd9dbf63606faa9ae3b10548d5b3656fb859')
if __name__ == '__main__':
- if is_running_under_teamcity():
- runner = TeamcityTestRunner()
- else:
- runner = unittest.TextTestRunner()
+ runner = unittest.TextTestRunner()
unittest.main(testRunner=runner)
diff --git a/tests/src/OneLogin/saml2_tests/authn_request_test.py b/tests/src/OneLogin/saml2_tests/authn_request_test.py
index be891cff..cd344890 100644
--- a/tests/src/OneLogin/saml2_tests/authn_request_test.py
+++ b/tests/src/OneLogin/saml2_tests/authn_request_test.py
@@ -7,8 +7,6 @@
import json
from os.path import dirname, join, exists
import unittest
-from teamcity import is_running_under_teamcity
-from teamcity.unittestpy import TeamcityTestRunner
from urlparse import urlparse, parse_qs
from zlib import decompress
@@ -341,8 +339,5 @@ def testAttributeConsumingService(self):
self.assertRegexpMatches(inflated, 'AttributeConsumingServiceIndex="1"')
if __name__ == '__main__':
- if is_running_under_teamcity():
- runner = TeamcityTestRunner()
- else:
- runner = unittest.TextTestRunner()
+ runner = unittest.TextTestRunner()
unittest.main(testRunner=runner)
diff --git a/tests/src/OneLogin/saml2_tests/error_test.py b/tests/src/OneLogin/saml2_tests/error_test.py
index cd997cea..9cc861ad 100644
--- a/tests/src/OneLogin/saml2_tests/error_test.py
+++ b/tests/src/OneLogin/saml2_tests/error_test.py
@@ -4,8 +4,6 @@
# MIT License
import unittest
-from teamcity import is_running_under_teamcity
-from teamcity.unittestpy import TeamcityTestRunner
from onelogin.saml2.errors import OneLogin_Saml2_Error
@@ -19,8 +17,5 @@ def runTest(self):
if __name__ == '__main__':
- if is_running_under_teamcity():
- runner = TeamcityTestRunner()
- else:
- runner = unittest.TextTestRunner()
+ runner = unittest.TextTestRunner()
unittest.main(testRunner=runner)
diff --git a/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py b/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py
index 4e0e79c2..6b11e74e 100644
--- a/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py
+++ b/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py
@@ -10,8 +10,6 @@
from lxml.etree import XMLSyntaxError
import unittest
from urllib2 import URLError
-from teamcity import is_running_under_teamcity
-from teamcity.unittestpy import TeamcityTestRunner
from onelogin.saml2.idp_metadata_parser import OneLogin_Saml2_IdPMetadataParser
from onelogin.saml2.constants import OneLogin_Saml2_Constants
@@ -645,8 +643,5 @@ def test_merge_settings(self):
self.assertEqual(expected_settings3, settings_result3)
if __name__ == '__main__':
- if is_running_under_teamcity():
- runner = TeamcityTestRunner()
- else:
- runner = unittest.TextTestRunner()
+ runner = unittest.TextTestRunner()
unittest.main(testRunner=runner)
diff --git a/tests/src/OneLogin/saml2_tests/logout_request_test.py b/tests/src/OneLogin/saml2_tests/logout_request_test.py
index 3ecf901a..cbeed68a 100644
--- a/tests/src/OneLogin/saml2_tests/logout_request_test.py
+++ b/tests/src/OneLogin/saml2_tests/logout_request_test.py
@@ -7,8 +7,6 @@
import json
from os.path import dirname, join, exists
import unittest
-from teamcity import is_running_under_teamcity
-from teamcity.unittestpy import TeamcityTestRunner
from urlparse import urlparse, parse_qs
from xml.dom.minidom import parseString
@@ -545,8 +543,5 @@ def testGetXML(self):
if __name__ == '__main__':
- if is_running_under_teamcity():
- runner = TeamcityTestRunner()
- else:
- runner = unittest.TextTestRunner()
+ runner = unittest.TextTestRunner()
unittest.main(testRunner=runner)
diff --git a/tests/src/OneLogin/saml2_tests/logout_response_test.py b/tests/src/OneLogin/saml2_tests/logout_response_test.py
index 27151a5a..13b17a5c 100644
--- a/tests/src/OneLogin/saml2_tests/logout_response_test.py
+++ b/tests/src/OneLogin/saml2_tests/logout_response_test.py
@@ -6,8 +6,6 @@
import json
from os.path import dirname, join, exists
import unittest
-from teamcity import is_running_under_teamcity
-from teamcity.unittestpy import TeamcityTestRunner
from urlparse import urlparse, parse_qs
from xml.dom.minidom import parseString
@@ -412,8 +410,5 @@ def testGetXML(self):
if __name__ == '__main__':
- if is_running_under_teamcity():
- runner = TeamcityTestRunner()
- else:
- runner = unittest.TextTestRunner()
+ runner = unittest.TextTestRunner()
unittest.main(testRunner=runner)
diff --git a/tests/src/OneLogin/saml2_tests/metadata_test.py b/tests/src/OneLogin/saml2_tests/metadata_test.py
index f69f5c7e..a63a552e 100644
--- a/tests/src/OneLogin/saml2_tests/metadata_test.py
+++ b/tests/src/OneLogin/saml2_tests/metadata_test.py
@@ -9,8 +9,6 @@
from time import strftime
from datetime import datetime
import unittest
-from teamcity import is_running_under_teamcity
-from teamcity.unittestpy import TeamcityTestRunner
from onelogin.saml2.metadata import OneLogin_Saml2_Metadata
from onelogin.saml2.settings import OneLogin_Saml2_Settings
@@ -290,8 +288,5 @@ def testAddX509KeyDescriptors(self):
if __name__ == '__main__':
- if is_running_under_teamcity():
- runner = TeamcityTestRunner()
- else:
- runner = unittest.TextTestRunner()
+ runner = unittest.TextTestRunner()
unittest.main(testRunner=runner)
diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py
index a67c8f62..c51bfcaa 100644
--- a/tests/src/OneLogin/saml2_tests/response_test.py
+++ b/tests/src/OneLogin/saml2_tests/response_test.py
@@ -10,8 +10,6 @@
import json
from os.path import dirname, join, exists
import unittest
-from teamcity import is_running_under_teamcity
-from teamcity.unittestpy import TeamcityTestRunner
from xml.dom.minidom import parseString
from lxml import etree
from onelogin.saml2.response import OneLogin_Saml2_Response
@@ -1679,8 +1677,5 @@ def testGetAssertionNotOnOrAfter(self):
if __name__ == '__main__':
- if is_running_under_teamcity():
- runner = TeamcityTestRunner()
- else:
- runner = unittest.TextTestRunner()
+ runner = unittest.TextTestRunner()
unittest.main(testRunner=runner)
diff --git a/tests/src/OneLogin/saml2_tests/settings_test.py b/tests/src/OneLogin/saml2_tests/settings_test.py
index 4b79715d..8d02d041 100644
--- a/tests/src/OneLogin/saml2_tests/settings_test.py
+++ b/tests/src/OneLogin/saml2_tests/settings_test.py
@@ -6,8 +6,6 @@
import json
from os.path import dirname, join, exists, sep
import unittest
-from teamcity import is_running_under_teamcity
-from teamcity.unittestpy import TeamcityTestRunner
from onelogin.saml2.errors import OneLogin_Saml2_Error
from onelogin.saml2.settings import OneLogin_Saml2_Settings
@@ -750,8 +748,5 @@ def testIsDebugActive(self):
if __name__ == '__main__':
- if is_running_under_teamcity():
- runner = TeamcityTestRunner()
- else:
- runner = unittest.TextTestRunner()
+ runner = unittest.TextTestRunner()
unittest.main(testRunner=runner)
diff --git a/tests/src/OneLogin/saml2_tests/signed_response_test.py b/tests/src/OneLogin/saml2_tests/signed_response_test.py
index 12db9143..77e0d5c4 100644
--- a/tests/src/OneLogin/saml2_tests/signed_response_test.py
+++ b/tests/src/OneLogin/saml2_tests/signed_response_test.py
@@ -7,8 +7,6 @@
import json
from os.path import dirname, join, exists
import unittest
-from teamcity import is_running_under_teamcity
-from teamcity.unittestpy import TeamcityTestRunner
from onelogin.saml2.response import OneLogin_Saml2_Response
from onelogin.saml2.settings import OneLogin_Saml2_Settings
@@ -62,8 +60,5 @@ def testResponseAndAssertionSigned(self):
if __name__ == '__main__':
- if is_running_under_teamcity():
- runner = TeamcityTestRunner()
- else:
- runner = unittest.TextTestRunner()
+ runner = unittest.TextTestRunner()
unittest.main(testRunner=runner)
diff --git a/tests/src/OneLogin/saml2_tests/utils_test.py b/tests/src/OneLogin/saml2_tests/utils_test.py
index 916c9c81..d0c73e86 100644
--- a/tests/src/OneLogin/saml2_tests/utils_test.py
+++ b/tests/src/OneLogin/saml2_tests/utils_test.py
@@ -9,8 +9,6 @@
from lxml import etree
from os.path import dirname, join, exists
import unittest
-from teamcity import is_running_under_teamcity
-from teamcity.unittestpy import TeamcityTestRunner
from xml.dom.minidom import Document, parseString
from onelogin.saml2.constants import OneLogin_Saml2_Constants
@@ -1029,8 +1027,5 @@ def testValidateSign(self):
if __name__ == '__main__':
- if is_running_under_teamcity():
- runner = TeamcityTestRunner()
- else:
- runner = unittest.TextTestRunner()
+ runner = unittest.TextTestRunner()
unittest.main(testRunner=runner)
From d8e0109e4c697f4a920ff4993c491eb5b0d38d55 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Thu, 26 Jul 2018 14:09:54 +0200
Subject: [PATCH 140/255] Update pylint dependency to 1.9.1
---
setup.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/setup.py b/setup.py
index 1c23c817..14b1b7b1 100644
--- a/setup.py
+++ b/setup.py
@@ -40,7 +40,7 @@
'test': (
'coverage>=3.6',
'freezegun==0.3.5',
- 'pylint==1.3.1',
+ 'pylint==1.9.1',
'pep8==1.5.7',
'pyflakes==0.8.1',
'coveralls==1.1',
From d1bd595a751027b779a0b78026aaa7aebbffc2e7 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Sun, 5 Aug 2018 21:13:25 +0200
Subject: [PATCH 141/255] Discourage the use of fingerprint on production
environments
---
README.md | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 8d31d913..64d01d1d 100644
--- a/README.md
+++ b/README.md
@@ -132,6 +132,8 @@ In production, the **strict** parameter MUST be set as **"true"**. Otherwise
your environment is not secure and will be exposed to attacks.
+In production also we highly recommend to register on the settings the IdP certificate instead of using the fingerprint method. The fingerprint, is a hash, so at the end is open to a collision attack that can end on a signature validation bypass. Other SAML toolkits deprecated that mechanism, we maintain it for compatibility and also to be used on test environment.
+
Getting started
---------------
@@ -326,7 +328,9 @@ This is the settings.json file:
"x509cert": ""
/*
* Instead of using the whole x509cert you can use a fingerprint in order to
- * validate a SAMLResponse, but you will need it to validate LogoutRequest and LogoutResponse using the HTTP-Redirect binding.
+ * validate a SAMLResponse (but you still need the x509cert to validate LogoutRequest and LogoutResponse using the HTTP-Redirect binding).
+ * 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 x509 -noout -fingerprint -in "idp.crt" to generate it,
* or add for example the -sha256 , -sha384 or -sha512 parameter)
@@ -337,6 +341,7 @@ This is the settings.json file:
*
* Notice that if you want to validate any SAML Message sent by the HTTP-Redirect binding, you
* will need to provide the whole x509cert.
+ *
*/
// 'certFingerprint': '',
// 'certFingerprintAlgorithm': 'sha1',
From d3dbe3e9788c312c79d78d4292b0d2792605a3c4 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Sun, 26 Aug 2018 13:00:17 +0200
Subject: [PATCH 142/255] Update dm.xmlsec.binding dependency to 1.3.7
---
setup.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/setup.py b/setup.py
index 14b1b7b1..cddaf740 100644
--- a/setup.py
+++ b/setup.py
@@ -32,7 +32,7 @@
},
test_suite='tests',
install_requires=[
- 'dm.xmlsec.binding==1.3.3',
+ 'dm.xmlsec.binding==1.3.7',
'isodate>=0.5.0',
'defusedxml>=0.4.1',
],
From 3814b0fe98d6ab78cf92b39c15e1785b1cab22bb Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Wed, 5 Sep 2018 20:02:21 +0200
Subject: [PATCH 143/255] Release 2.4.2
---
changelog.md | 5 +++++
setup.py | 2 +-
2 files changed, 6 insertions(+), 1 deletion(-)
diff --git a/changelog.md b/changelog.md
index d17975f1..996779c1 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,4 +1,9 @@
# python-saml changelog
+### 2.4.2 (Sep 05, 2018)
+* Update dm.xmlsec.binding dependency to 1.3.7
+* Update pylint dependency to 1.9.1
+* Update Django demo to use LTS version of Django
+
### 2.4.1 (Apr 25, 2018)
* Add ID to EntityDescriptor before sign it on add_sign method. Improve the way ds namespace is handled in add_sign method
* Update defusedxml, coveralls and coverage dependencies
diff --git a/setup.py b/setup.py
index cddaf740..cb53f643 100644
--- a/setup.py
+++ b/setup.py
@@ -9,7 +9,7 @@
setup(
name='python-saml',
- version='2.4.1',
+ version='2.4.2',
description='Onelogin Python Toolkit. Add SAML support to your Python software using this library',
classifiers=[
'Development Status :: 5 - Production/Stable',
From 4bafe94a70989b1a9cb49351e7615df11a72a9b7 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Thu, 27 Sep 2018 16:48:26 +0200
Subject: [PATCH 144/255] Fix #238 DSA constant
---
src/onelogin/saml2/constants.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/onelogin/saml2/constants.py b/src/onelogin/saml2/constants.py
index 895c248a..87cb0e6f 100644
--- a/src/onelogin/saml2/constants.py
+++ b/src/onelogin/saml2/constants.py
@@ -103,7 +103,7 @@ class OneLogin_Saml2_Constants(object):
SHA384 = 'http://www.w3.org/2001/04/xmldsig-more#sha384'
SHA512 = 'http://www.w3.org/2001/04/xmlenc#sha512'
- DSA_SHA1 = 'http://www.w3.org/2000/09/xmld/sig#dsa-sha1'
+ DSA_SHA1 = 'http://www.w3.org/2000/09/xmldsig#dsa-sha1'
RSA_SHA1 = 'http://www.w3.org/2000/09/xmldsig#rsa-sha1'
RSA_SHA256 = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256'
RSA_SHA384 = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha384'
From c7e5cda0225ba3517a85b1fc726323ea0e1ef616 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Wed, 17 Oct 2018 01:13:28 +0200
Subject: [PATCH 145/255] If debug enable, print reason for the SAMLResponse
invalidation
---
demo-django/demo/views.py | 7 ++++++-
demo-django/templates/index.html | 3 +++
2 files changed, 9 insertions(+), 1 deletion(-)
diff --git a/demo-django/demo/views.py b/demo-django/demo/views.py
index 59ad2a05..2ae0ebac 100644
--- a/demo-django/demo/views.py
+++ b/demo-django/demo/views.py
@@ -34,6 +34,7 @@ def index(request):
req = prepare_django_request(request)
auth = init_saml_auth(req)
errors = []
+ error_reason = None
not_auth_warn = False
success_slo = False
attributes = False
@@ -70,6 +71,7 @@ def index(request):
auth.process_response(request_id=request_id)
errors = auth.get_errors()
not_auth_warn = not auth.is_authenticated()
+
if not errors:
if 'AuthNRequestID' in request.session:
del request.session['AuthNRequestID']
@@ -78,6 +80,9 @@ def index(request):
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']))
+ else:
+ if auth.get_settings().is_debug_active():
+ error_reason = auth.get_last_error_reason()
elif 'sls' in req['get_data']:
request_id = None
if 'LogoutRequestID' in request.session:
@@ -96,7 +101,7 @@ def index(request):
if len(request.session['samlUserdata']) > 0:
attributes = request.session['samlUserdata'].items()
- return render(request, 'index.html', {'errors': errors, 'not_auth_warn': not_auth_warn, 'success_slo': success_slo,
+ return render(request, 'index.html', {'errors': errors, 'error_reason': error_reason, not_auth_warn: not_auth_warn, 'success_slo': success_slo,
'attributes': attributes, 'paint_logout': paint_logout})
diff --git a/demo-django/templates/index.html b/demo-django/templates/index.html
index f7d51101..87f2f08b 100644
--- a/demo-django/templates/index.html
+++ b/demo-django/templates/index.html
@@ -10,6 +10,9 @@
{{err}}
{% endfor %}
+ {% if error_reason %}
+ Reason: {{error_reason}}
+ {% endif %}
{% endif %}
From d0c319d7dd4e6ab87e6226c8f6a133470ad40ca0 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Fri, 2 Nov 2018 12:39:19 +0100
Subject: [PATCH 146/255] Don't require compression on LogoutResponse messages
by relaxing the decode_base64_and_inflate method
---
src/onelogin/saml2/utils.py | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py
index 4ae37efb..64b0bb6a 100644
--- a/src/onelogin/saml2/utils.py
+++ b/src/onelogin/saml2/utils.py
@@ -98,8 +98,14 @@ def decode_base64_and_inflate(value):
:returns: the string after decoding and inflating
:rtype: string
"""
+ decoded = base64.b64decode(value)
+ # We try to inflate
+ try:
+ result = zlib.decompress(decoded, -15)
+ except Exception:
+ result = decoded
- return zlib.decompress(base64.b64decode(value), -15).decode('utf-8')
+ return result.decode('utf-8')
@staticmethod
def deflate_and_base64_encode(value):
From d7370622a4479b1b933d29a965e4720fa11391d3 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Fri, 2 Nov 2018 13:27:33 +0100
Subject: [PATCH 147/255] Add expected/received in WRONG_ISSUER error
---
src/onelogin/saml2/logout_request.py | 6 +++++-
src/onelogin/saml2/logout_response.py | 6 +++++-
src/onelogin/saml2/response.py | 6 +++++-
tests/src/OneLogin/saml2_tests/logout_response_test.py | 4 ++--
tests/src/OneLogin/saml2_tests/response_test.py | 4 ++--
5 files changed, 19 insertions(+), 7 deletions(-)
diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py
index 5945c652..a566f489 100644
--- a/src/onelogin/saml2/logout_request.py
+++ b/src/onelogin/saml2/logout_request.py
@@ -367,7 +367,11 @@ def is_valid(self, request_data, raise_exceptions=False):
issuer = OneLogin_Saml2_Logout_Request.get_issuer(dom)
if issuer is not None and issuer != idp_entity_id:
raise OneLogin_Saml2_ValidationError(
- 'Invalid issuer in the Logout Request',
+ 'Invalid issuer in the Logout Request (expected %(idpEntityId)s, got %(issuer)s)' %
+ {
+ 'idpEntityId': idp_entity_id,
+ 'issuer': issuer
+ },
OneLogin_Saml2_ValidationError.WRONG_ISSUER
)
diff --git a/src/onelogin/saml2/logout_response.py b/src/onelogin/saml2/logout_response.py
index 75ade5e8..7eaf4cba 100644
--- a/src/onelogin/saml2/logout_response.py
+++ b/src/onelogin/saml2/logout_response.py
@@ -114,7 +114,11 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False):
issuer = self.get_issuer()
if issuer is not None and issuer != idp_entity_id:
raise OneLogin_Saml2_ValidationError(
- 'Invalid issuer in the Logout Request',
+ 'Invalid issuer in the Logout Response (expected %(idpEntityId)s, got %(issuer)s)' %
+ {
+ 'idpEntityId': idp_entity_id,
+ 'issuer': issuer
+ },
OneLogin_Saml2_ValidationError.WRONG_ISSUER
)
diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py
index b9eb3f8e..11a1ba47 100644
--- a/src/onelogin/saml2/response.py
+++ b/src/onelogin/saml2/response.py
@@ -224,7 +224,11 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False):
for issuer in issuers:
if issuer is None or issuer != idp_entity_id:
raise OneLogin_Saml2_ValidationError(
- 'Invalid issuer in the Assertion/Response',
+ 'Invalid issuer in the Assertion/Response (expected %(idpEntityId)s, got %(issuer)s)' %
+ {
+ 'idpEntityId': idp_entity_id,
+ 'issuer': issuer
+ },
OneLogin_Saml2_ValidationError.WRONG_ISSUER
)
diff --git a/tests/src/OneLogin/saml2_tests/logout_response_test.py b/tests/src/OneLogin/saml2_tests/logout_response_test.py
index 13b17a5c..6493e534 100644
--- a/tests/src/OneLogin/saml2_tests/logout_response_test.py
+++ b/tests/src/OneLogin/saml2_tests/logout_response_test.py
@@ -186,7 +186,7 @@ def testIsInValidIssuer(self):
settings.set_strict(True)
response_2 = OneLogin_Saml2_Logout_Response(settings, message)
self.assertFalse(response_2.is_valid(request_data))
- self.assertIn('Invalid issuer in the Logout Request', response_2.get_error())
+ self.assertIn('Invalid issuer in the Logout Response', response_2.get_error())
def testIsInValidDestination(self):
"""
@@ -272,7 +272,7 @@ def testIsInValidSign(self):
settings.set_strict(True)
response_2 = OneLogin_Saml2_Logout_Response(settings, request_data['get_data']['SAMLResponse'])
self.assertFalse(response_2.is_valid(request_data))
- self.assertIn('Invalid issuer in the Logout Request', response_2.get_error())
+ self.assertIn('Invalid issuer in the Logout Response', response_2.get_error())
settings.set_strict(False)
old_signature = request_data['get_data']['Signature']
diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py
index c51bfcaa..d6daaf97 100644
--- a/tests/src/OneLogin/saml2_tests/response_test.py
+++ b/tests/src/OneLogin/saml2_tests/response_test.py
@@ -1012,11 +1012,11 @@ def testIsInValidIssuer(self):
settings.set_strict(True)
response_3 = OneLogin_Saml2_Response(settings, message)
self.assertFalse(response_3.is_valid(request_data))
- self.assertEqual('Invalid issuer in the Assertion/Response', response_3.get_error())
+ self.assertEqual('Invalid issuer in the Assertion/Response (expected http://idp.example.com/, got http://invalid.issuer.example.com/)', response_3.get_error())
response_4 = OneLogin_Saml2_Response(settings, message_2)
self.assertFalse(response_4.is_valid(request_data))
- self.assertEqual('Invalid issuer in the Assertion/Response', response_4.get_error())
+ self.assertEqual('Invalid issuer in the Assertion/Response (expected http://idp.example.com/, got http://invalid.isser.example.com/)', response_4.get_error())
def testIsInValidSessionIndex(self):
"""
From ec5ac009594c87fd3451e7fe0ba648d0fc9bcdd3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marcin=20Ku=C5=BAmi=C5=84ski?=
Date: Fri, 9 Nov 2018 17:26:47 +0100
Subject: [PATCH 148/255] auth: fix docstring
Fixed small docstring mismatch
---
src/onelogin/saml2/auth.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py
index 8022ca24..eb61705e 100644
--- a/src/onelogin/saml2/auth.py
+++ b/src/onelogin/saml2/auth.py
@@ -42,8 +42,8 @@ def __init__(self, request_data, old_settings=None, custom_base_path=None):
:param request_data: Request Data
:type request_data: dict
- :param settings: Optional. SAML Toolkit Settings
- :type settings: dict
+ :param old_settings: Optional. SAML Toolkit Settings
+ :type old_settings: dict
:param custom_base_path: Optional. Path where are stored the settings file and the cert folder
:type custom_base_path: string
From 5fc6294db4911d7ee67ba719ada0d661ba8ccd4a Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Wed, 14 Nov 2018 18:29:05 +0100
Subject: [PATCH 149/255] Fixed a ValidationError misspelling
---
src/onelogin/saml2/response.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py
index 11a1ba47..9e96288a 100644
--- a/src/onelogin/saml2/response.py
+++ b/src/onelogin/saml2/response.py
@@ -647,7 +647,7 @@ def process_signed_elements(self):
if not self.validate_signed_elements(signed_elements, raise_exceptions=True):
raise OneLogin_Saml2_ValidationError(
'Found an unexpected Signature Element. SAML Response rejected',
- OneLogin_Saml2_ValidationError.UNEXPECTED_SIGNED_ELEMENT
+ OneLogin_Saml2_ValidationError.UNEXPECTED_SIGNED_ELEMENTS
)
return signed_elements
From 0f7652e550d3a17d65de8c3421b289830aad3c4c Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Sat, 15 Dec 2018 23:31:07 +0100
Subject: [PATCH 150/255] Update Shibboleth XML URL
---
.../src/OneLogin/saml2_tests/idp_metadata_parser_test.py | 9 ++++-----
1 file changed, 4 insertions(+), 5 deletions(-)
diff --git a/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py b/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py
index 6b11e74e..33030cd3 100644
--- a/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py
+++ b/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py
@@ -48,7 +48,7 @@ def testGetMetadata(self):
data = OneLogin_Saml2_IdPMetadataParser.get_metadata('http://google.es')
try:
- data = OneLogin_Saml2_IdPMetadataParser.get_metadata('https://www.testshib.org/metadata/testshib-providers.xml')
+ data = OneLogin_Saml2_IdPMetadataParser.get_metadata('https://idp.testshib.org/idp/shibboleth')
except URLError:
data = self.file_contents(join(self.data_path, 'metadata', 'testshib-providers.xml'))
self.assertTrue(data is not None and data is not {})
@@ -61,7 +61,7 @@ def testParseRemote(self):
data = OneLogin_Saml2_IdPMetadataParser.parse_remote('http://google.es')
try:
- data = OneLogin_Saml2_IdPMetadataParser.parse_remote('https://www.testshib.org/metadata/testshib-providers.xml')
+ data = OneLogin_Saml2_IdPMetadataParser.parse_remote('https://idp.testshib.org/idp/shibboleth')
except URLError:
xml = self.file_contents(join(self.data_path, 'metadata', 'testshib-providers.xml'))
data = OneLogin_Saml2_IdPMetadataParser.parse(xml)
@@ -83,7 +83,6 @@ def testParseRemote(self):
}
"""
expected_settings = json.loads(expected_settings_json)
-
self.assertEqual(expected_settings, data)
def testParse(self):
@@ -144,7 +143,7 @@ def test_parse_testshib_required_binding_sso_redirect(self):
"""
try:
xmldoc = OneLogin_Saml2_IdPMetadataParser.get_metadata(
- 'https://www.testshib.org/metadata/testshib-providers.xml')
+ 'https://idp.testshib.org/idp/shibboleth')
except Exception:
xmldoc = self.file_contents(join(self.data_path, 'metadata', 'testshib-providers.xml'))
@@ -181,7 +180,7 @@ def test_parse_testshib_required_binding_sso_post(self):
"""
try:
xmldoc = OneLogin_Saml2_IdPMetadataParser.get_metadata(
- 'https://www.testshib.org/metadata/testshib-providers.xml')
+ 'https://idp.testshib.org/idp/shibboleth')
except URLError:
xmldoc = self.file_contents(join(self.data_path, 'metadata', 'testshib-providers.xml'))
From b81db0b69877ef958b89308618a65d9f7441f25c Mon Sep 17 00:00:00 2001
From: cclauss
Date: Fri, 18 Jan 2019 04:06:09 +0100
Subject: [PATCH 151/255] setup.py: flake8 is a superset of pep8 and pyflakes
---
setup.py | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/setup.py b/setup.py
index cb53f643..c3f16089 100644
--- a/setup.py
+++ b/setup.py
@@ -41,8 +41,7 @@
'coverage>=3.6',
'freezegun==0.3.5',
'pylint==1.9.1',
- 'pep8==1.5.7',
- 'pyflakes==0.8.1',
+ 'flake8==3.6.0',
'coveralls==1.1',
),
},
From e24ddca007fae344048235d6adc20f958dcacbb0 Mon Sep 17 00:00:00 2001
From: cclauss
Date: Fri, 18 Jan 2019 04:15:57 +0100
Subject: [PATCH 152/255] pep8 --> pycodestyle
https://pep8.readthedocs.io/en/release-1.7.x/
---
.travis.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.travis.yml b/.travis.yml
index 4f275e8d..7221c335 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -13,7 +13,7 @@ script:
- 'coverage run --source=src/onelogin/saml2 --rcfile=tests/coverage.rc setup.py test'
- 'coverage report -m --rcfile=tests/coverage.rc'
# - 'pylint src/onelogin/saml2 --rcfile=tests/pylint.rc'
- - 'pep8 tests/src/OneLogin/saml2_tests/*.py demo-flask/*.py demo-django/*.py src/onelogin/saml2/*.py --config=tests/pep8.rc'
+ - 'pycodestyle tests/src/OneLogin/saml2_tests/*.py demo-flask/*.py demo-django/*.py src/onelogin/saml2/*.py --config=tests/pep8.rc'
- 'pyflakes src/onelogin/saml2 demo-django demo-flask tests/src/OneLogin/saml2_tests'
after_success: 'coveralls'
From a3fe9b83686896808f1753d94fd1349b4479b4b6 Mon Sep 17 00:00:00 2001
From: cclauss
Date: Fri, 18 Jan 2019 04:23:20 +0100
Subject: [PATCH 153/255] Update pep8.rc
---
tests/pep8.rc | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tests/pep8.rc b/tests/pep8.rc
index 910e1eaa..1f1d592f 100644
--- a/tests/pep8.rc
+++ b/tests/pep8.rc
@@ -1,3 +1,3 @@
-[pep8]
+[pycodestyle]
ignore = E501, E731
max-line-length = 160
From 606e21a26e1bef3178a22df1d34b61eec76bafc1 Mon Sep 17 00:00:00 2001
From: cclauss
Date: Fri, 18 Jan 2019 04:36:11 +0100
Subject: [PATCH 154/255] Pycodestyle W605: Use r'strings' instead of 'strings'
---
tests/src/OneLogin/saml2_tests/authn_request_test.py | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/tests/src/OneLogin/saml2_tests/authn_request_test.py b/tests/src/OneLogin/saml2_tests/authn_request_test.py
index cd344890..3dd3e9df 100644
--- a/tests/src/OneLogin/saml2_tests/authn_request_test.py
+++ b/tests/src/OneLogin/saml2_tests/authn_request_test.py
@@ -272,7 +272,7 @@ def testCreateDeflatedSAMLRequestURLParameter(self):
'SAMLRequest': authn_request.get_request()
}
auth_url = OneLogin_Saml2_Utils.redirect('http://idp.example.com/SSOService.php', parameters, True)
- self.assertRegexpMatches(auth_url, '^http://idp\.example\.com\/SSOService\.php\?SAMLRequest=')
+ self.assertRegexpMatches(auth_url, r'^http://idp\.example\.com\/SSOService\.php\?SAMLRequest=')
exploded = urlparse(auth_url)
exploded = parse_qs(exploded[4])
payload = exploded['SAMLRequest'][0]
@@ -301,7 +301,7 @@ def testCreateEncSAMLRequest(self):
'SAMLRequest': authn_request.get_request()
}
auth_url = OneLogin_Saml2_Utils.redirect('http://idp.example.com/SSOService.php', parameters, True)
- self.assertRegexpMatches(auth_url, '^http://idp\.example\.com\/SSOService\.php\?SAMLRequest=')
+ self.assertRegexpMatches(auth_url, r'^http://idp\.example\.com\/SSOService\.php\?SAMLRequest=')
exploded = urlparse(auth_url)
exploded = parse_qs(exploded[4])
payload = exploded['SAMLRequest'][0]
@@ -338,6 +338,7 @@ def testAttributeConsumingService(self):
self.assertRegexpMatches(inflated, 'AttributeConsumingServiceIndex="1"')
+
if __name__ == '__main__':
runner = unittest.TextTestRunner()
unittest.main(testRunner=runner)
From ff3b07c20cd579a08cedfc8a24b5964115a89b4a Mon Sep 17 00:00:00 2001
From: cclauss
Date: Fri, 18 Jan 2019 04:44:08 +0100
Subject: [PATCH 155/255] r'strings'
---
tests/src/OneLogin/saml2_tests/logout_request_test.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/tests/src/OneLogin/saml2_tests/logout_request_test.py b/tests/src/OneLogin/saml2_tests/logout_request_test.py
index cbeed68a..eb50c785 100644
--- a/tests/src/OneLogin/saml2_tests/logout_request_test.py
+++ b/tests/src/OneLogin/saml2_tests/logout_request_test.py
@@ -46,7 +46,7 @@ def testConstructor(self):
parameters = {'SAMLRequest': logout_request.get_request()}
logout_url = OneLogin_Saml2_Utils.redirect('http://idp.example.com/SingleLogoutService.php', parameters, True)
- self.assertRegexpMatches(logout_url, '^http://idp\.example\.com\/SingleLogoutService\.php\?SAMLRequest=')
+ self.assertRegexpMatches(logout_url, r'^http://idp\.example\.com\/SingleLogoutService\.php\?SAMLRequest=')
url_parts = urlparse(logout_url)
exploded = parse_qs(url_parts.query)
payload = exploded['SAMLRequest'][0]
@@ -63,7 +63,7 @@ def testCreateDeflatedSAMLLogoutRequestURLParameter(self):
parameters = {'SAMLRequest': logout_request.get_request()}
logout_url = OneLogin_Saml2_Utils.redirect('http://idp.example.com/SingleLogoutService.php', parameters, True)
- self.assertRegexpMatches(logout_url, '^http://idp\.example\.com\/SingleLogoutService\.php\?SAMLRequest=')
+ self.assertRegexpMatches(logout_url, r'^http://idp\.example\.com\/SingleLogoutService\.php\?SAMLRequest=')
url_parts = urlparse(logout_url)
exploded = parse_qs(url_parts.query)
payload = exploded['SAMLRequest'][0]
@@ -120,7 +120,7 @@ def testConstructorEncryptIdUsingX509certMulti(self):
parameters = {'SAMLRequest': logout_request.get_request()}
logout_url = OneLogin_Saml2_Utils.redirect('http://idp.example.com/SingleLogoutService.php', parameters, True)
- self.assertRegexpMatches(logout_url, '^http://idp\.example\.com\/SingleLogoutService\.php\?SAMLRequest=')
+ self.assertRegexpMatches(logout_url, r'^http://idp\.example\.com\/SingleLogoutService\.php\?SAMLRequest=')
url_parts = urlparse(logout_url)
exploded = parse_qs(url_parts.query)
payload = exploded['SAMLRequest'][0]
From 98fa1ee5f2fa785db5671db9f94653f6f40f6cef Mon Sep 17 00:00:00 2001
From: cclauss
Date: Fri, 18 Jan 2019 04:45:13 +0100
Subject: [PATCH 156/255] r'strings'
---
tests/src/OneLogin/saml2_tests/logout_response_test.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tests/src/OneLogin/saml2_tests/logout_response_test.py b/tests/src/OneLogin/saml2_tests/logout_response_test.py
index 6493e534..38d9fab6 100644
--- a/tests/src/OneLogin/saml2_tests/logout_response_test.py
+++ b/tests/src/OneLogin/saml2_tests/logout_response_test.py
@@ -56,7 +56,7 @@ def testCreateDeflatedSAMLLogoutResponseURLParameter(self):
logout_url = OneLogin_Saml2_Utils.redirect('http://idp.example.com/SingleLogoutService.php', parameters, True)
- self.assertRegexpMatches(logout_url, '^http://idp\.example\.com\/SingleLogoutService\.php\?SAMLResponse=')
+ self.assertRegexpMatches(logout_url, r'^http://idp\.example\.com\/SingleLogoutService\.php\?SAMLResponse=')
url_parts = urlparse(logout_url)
exploded = parse_qs(url_parts.query)
inflated = OneLogin_Saml2_Utils.decode_base64_and_inflate(exploded['SAMLResponse'][0])
From 4948c7e1a38daabb4e29bb87c9cbf2c5f15a4e8e Mon Sep 17 00:00:00 2001
From: cclauss
Date: Fri, 18 Jan 2019 04:51:13 +0100
Subject: [PATCH 157/255] Do not use bare except
---
src/onelogin/saml2/idp_metadata_parser.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/onelogin/saml2/idp_metadata_parser.py b/src/onelogin/saml2/idp_metadata_parser.py
index 3b5637a5..ad10661c 100644
--- a/src/onelogin/saml2/idp_metadata_parser.py
+++ b/src/onelogin/saml2/idp_metadata_parser.py
@@ -54,7 +54,7 @@ def get_metadata(url, validate_cert=True):
idp_descriptor_nodes = OneLogin_Saml2_Utils.query(dom, '//md:IDPSSODescriptor')
if idp_descriptor_nodes:
valid = True
- except:
+ except Exception:
pass
if not valid:
From 31129e8ecff8273428a54f6fc6d9dd8142869dcd Mon Sep 17 00:00:00 2001
From: cclauss
Date: Fri, 18 Jan 2019 04:53:27 +0100
Subject: [PATCH 158/255] ignore = W504 line break after binary operator
---
tests/pep8.rc | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tests/pep8.rc b/tests/pep8.rc
index 1f1d592f..ad7c254b 100644
--- a/tests/pep8.rc
+++ b/tests/pep8.rc
@@ -1,3 +1,3 @@
[pycodestyle]
-ignore = E501, E731
+ignore = E501, E731, W504
max-line-length = 160
From 2a08d00b69d1c9820f1ec5b23adab4c071b0aa3c Mon Sep 17 00:00:00 2001
From: cclauss
Date: Fri, 18 Jan 2019 04:55:45 +0100
Subject: [PATCH 159/255] E305 expected 2 blank lines after class or function
definition
---
tests/src/OneLogin/saml2_tests/auth_test.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py
index 5bc13000..2b946553 100644
--- a/tests/src/OneLogin/saml2_tests/auth_test.py
+++ b/tests/src/OneLogin/saml2_tests/auth_test.py
@@ -1163,6 +1163,7 @@ def testGetIdFromLogoutResponse(self):
auth.process_slo()
self.assertIn(auth.get_last_message_id(), '_f9ee61bd9dbf63606faa9ae3b10548d5b3656fb859')
+
if __name__ == '__main__':
runner = unittest.TextTestRunner()
unittest.main(testRunner=runner)
From 581ef965a2795b7d23462f89276a5dcdf726ac9d Mon Sep 17 00:00:00 2001
From: cclauss
Date: Fri, 18 Jan 2019 04:58:12 +0100
Subject: [PATCH 160/255] r'strings'
---
src/onelogin/saml2/utils.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py
index 64b0bb6a..565bb50c 100644
--- a/src/onelogin/saml2/utils.py
+++ b/src/onelogin/saml2/utils.py
@@ -434,7 +434,7 @@ def generate_unique_id():
@staticmethod
def parse_time_to_SAML(time):
- """
+ r"""
Converts a UNIX timestamp to SAML2 timestamp on the form
yyyy-mm-ddThh:mm:ss(\.s+)?Z.
@@ -449,7 +449,7 @@ def parse_time_to_SAML(time):
@staticmethod
def parse_SAML_to_time(timestr):
- """
+ r"""
Converts a SAML2 timestamp on the form yyyy-mm-ddThh:mm:ss(\.s+)?Z
to a UNIX timestamp. The sub-second part is ignored.
From 941dd9d10ecc81ced4ce063a9ccd4fcb2e92ca0d Mon Sep 17 00:00:00 2001
From: cclauss
Date: Fri, 18 Jan 2019 05:03:15 +0100
Subject: [PATCH 161/255] expected 2 blank lines after class or function
definition
---
tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py b/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py
index 33030cd3..0359bb5a 100644
--- a/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py
+++ b/tests/src/OneLogin/saml2_tests/idp_metadata_parser_test.py
@@ -641,6 +641,7 @@ def test_merge_settings(self):
expected_settings3 = json.loads(expected_settings3_json)
self.assertEqual(expected_settings3, settings_result3)
+
if __name__ == '__main__':
runner = unittest.TextTestRunner()
unittest.main(testRunner=runner)
From c2e8b095e1af5fe665d5128dfb4ed53e21e92ad8 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Tue, 29 Jan 2019 13:11:29 +0100
Subject: [PATCH 162/255] Security improvements. Use of tagid to prevent XPath
injection. Disable DTD on fromstring defusedxml method
---
src/onelogin/saml2/idp_metadata_parser.py | 4 +-
src/onelogin/saml2/logout_request.py | 10 ++---
src/onelogin/saml2/logout_response.py | 4 +-
src/onelogin/saml2/metadata.py | 2 +-
src/onelogin/saml2/response.py | 20 ++++++----
src/onelogin/saml2/utils.py | 46 +++++++++++++----------
6 files changed, 49 insertions(+), 37 deletions(-)
diff --git a/src/onelogin/saml2/idp_metadata_parser.py b/src/onelogin/saml2/idp_metadata_parser.py
index ad10661c..55055921 100644
--- a/src/onelogin/saml2/idp_metadata_parser.py
+++ b/src/onelogin/saml2/idp_metadata_parser.py
@@ -50,7 +50,7 @@ def get_metadata(url, validate_cert=True):
if xml:
try:
- dom = fromstring(xml)
+ dom = fromstring(xml, forbid_dtd=True)
idp_descriptor_nodes = OneLogin_Saml2_Utils.query(dom, '//md:IDPSSODescriptor')
if idp_descriptor_nodes:
valid = True
@@ -124,7 +124,7 @@ def parse(
"""
data = {}
- dom = fromstring(idp_metadata)
+ dom = fromstring(idp_metadata, forbid_dtd=True)
entity_desc_path = '//md:EntityDescriptor'
if entity_id:
diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py
index a566f489..f201ff36 100644
--- a/src/onelogin/saml2/logout_request.py
+++ b/src/onelogin/saml2/logout_request.py
@@ -171,7 +171,7 @@ def get_id(request):
else:
if isinstance(request, Document):
request = request.toxml()
- elem = fromstring(request)
+ elem = fromstring(request, forbid_dtd=True)
return elem.get('ID', None)
@staticmethod
@@ -190,7 +190,7 @@ def get_nameid_data(request, key=None):
else:
if isinstance(request, Document):
request = request.toxml()
- elem = fromstring(request)
+ elem = fromstring(request, forbid_dtd=True)
name_id = None
encrypted_entries = OneLogin_Saml2_Utils.query(elem, '/samlp:LogoutRequest/saml:EncryptedID')
@@ -271,7 +271,7 @@ def get_issuer(request):
else:
if isinstance(request, Document):
request = request.toxml()
- elem = fromstring(request)
+ elem = fromstring(request, forbid_dtd=True)
issuer = None
issuer_nodes = OneLogin_Saml2_Utils.query(elem, '/samlp:LogoutRequest/saml:Issuer')
@@ -293,7 +293,7 @@ def get_session_indexes(request):
else:
if isinstance(request, Document):
request = request.toxml()
- elem = fromstring(request)
+ elem = fromstring(request, forbid_dtd=True)
session_indexes = []
session_index_nodes = OneLogin_Saml2_Utils.query(elem, '/samlp:LogoutRequest/samlp:SessionIndex')
@@ -314,7 +314,7 @@ def is_valid(self, request_data, raise_exceptions=False):
self.__error = None
lowercase_urlencoding = False
try:
- dom = fromstring(self.__logout_request)
+ dom = fromstring(self.__logout_request, forbid_dtd=True)
idp_data = self.__settings.get_idp_data()
idp_entity_id = idp_data['entityId']
diff --git a/src/onelogin/saml2/logout_response.py b/src/onelogin/saml2/logout_response.py
index 7eaf4cba..3e6be91f 100644
--- a/src/onelogin/saml2/logout_response.py
+++ b/src/onelogin/saml2/logout_response.py
@@ -44,7 +44,7 @@ def __init__(self, settings, response=None):
if response is not None:
self.__logout_response = OneLogin_Saml2_Utils.decode_base64_and_inflate(response)
- self.document = parseString(self.__logout_response)
+ self.document = parseString(self.__logout_response, forbid_dtd=True)
self.id = self.document.documentElement.getAttribute('ID')
def get_issuer(self):
@@ -200,7 +200,7 @@ def __query(self, query):
"""
# Switch to lxml for querying
xml = self.document.toxml()
- return OneLogin_Saml2_Utils.query(fromstring(xml), query)
+ return OneLogin_Saml2_Utils.query(fromstring(xml, forbid_dtd=True), query)
def build(self, in_response_to):
"""
diff --git a/src/onelogin/saml2/metadata.py b/src/onelogin/saml2/metadata.py
index 212c0e95..9a931c9e 100644
--- a/src/onelogin/saml2/metadata.py
+++ b/src/onelogin/saml2/metadata.py
@@ -247,7 +247,7 @@ def add_x509_key_descriptors(metadata, cert=None, add_encryption=True):
if cert is None or cert == '':
return metadata
try:
- xml = parseString(metadata.encode('utf-8'))
+ xml = parseString(metadata.encode('utf-8'), forbid_dtd=True)
except Exception as e:
raise Exception('Error parsing metadata. ' + e.message)
diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py
index 9e96288a..da9c1d77 100644
--- a/src/onelogin/saml2/response.py
+++ b/src/onelogin/saml2/response.py
@@ -40,7 +40,7 @@ def __init__(self, settings, response):
self.__settings = settings
self.__error = None
self.response = b64decode(response)
- self.document = fromstring(self.response)
+ self.document = fromstring(self.response, forbid_dtd=True)
self.decrypted_document = None
self.encrypted = None
self.valid_scd_not_on_or_after = None
@@ -735,6 +735,7 @@ def __query_assertion(self, xpath_expr):
signature_expr = '/ds:Signature/ds:SignedInfo/ds:Reference'
signed_assertion_query = '/samlp:Response' + assertion_expr + signature_expr
assertion_reference_nodes = self.__query(signed_assertion_query)
+ tagid = None
if not assertion_reference_nodes:
# Check if the message is signed
@@ -742,23 +743,28 @@ def __query_assertion(self, xpath_expr):
message_reference_nodes = self.__query(signed_message_query)
if message_reference_nodes:
message_id = message_reference_nodes[0].get('URI')
- final_query = "/samlp:Response[@ID='%s']/" % message_id[1:]
+ final_query = "/samlp:Response[@ID=$tagid]/"
+ tagid = message_id[1:]
else:
final_query = "/samlp:Response"
final_query += assertion_expr
else:
assertion_id = assertion_reference_nodes[0].get('URI')
- final_query = '/samlp:Response' + assertion_expr + "[@ID='%s']" % assertion_id[1:]
+ final_query = '/samlp:Response' + assertion_expr + "[@ID=$tagid]"
+ tagid = assertion_id[1:]
final_query += xpath_expr
- return self.__query(final_query)
+ return self.__query(final_query, tagid)
- def __query(self, query):
+ def __query(self, query, tagid=None):
"""
Extracts nodes that match the query from the Response
:param query: Xpath Expresion
:type query: String
+ :param tagid: Tag ID
+ :type query: String
+
:returns: The queried nodes
:rtype: list
"""
@@ -766,7 +772,7 @@ def __query(self, query):
document = self.decrypted_document
else:
document = self.document
- return OneLogin_Saml2_Utils.query(document, query)
+ return OneLogin_Saml2_Utils.query(document, query, None, tagid)
def __decrypt_assertion(self, dom):
"""
@@ -817,7 +823,7 @@ def __decrypt_assertion(self, dom):
if not uri.startswith('#'):
break
uri = uri.split('#')[1]
- encrypted_key = OneLogin_Saml2_Utils.query(encrypted_assertion_nodes[0], './xenc:EncryptedKey[@Id="' + uri + '"]')
+ encrypted_key = OneLogin_Saml2_Utils.query(encrypted_assertion_nodes[0], './xenc:EncryptedKey[@Id=$tagid]', None, uri)
if encrypted_key:
keyinfo.append(encrypted_key[0])
diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py
index 565bb50c..8d11447a 100644
--- a/src/onelogin/saml2/utils.py
+++ b/src/onelogin/saml2/utils.py
@@ -141,7 +141,7 @@ def validate_xml(xml, schema, debug=False):
# Switch to lxml for schema validation
try:
- dom = fromstring(xml.encode('utf-8'))
+ dom = fromstring(xml.encode('utf-8'), forbid_dtd=True)
except Exception:
return 'unloaded_xml'
@@ -160,7 +160,7 @@ def validate_xml(xml, schema, debug=False):
return 'invalid_xml'
- return parseString(tostring(dom, encoding='unicode').encode('utf-8'))
+ return parseString(tostring(dom, encoding='unicode').encode('utf-8'), forbid_dtd=True)
@staticmethod
def element_text(node):
@@ -530,7 +530,7 @@ def get_expire_time(cache_duration=None, valid_until=None):
return None
@staticmethod
- def query(dom, query, context=None):
+ def query(dom, query, context=None, tagid=None):
"""
Extracts nodes that match the query from the Element
@@ -543,13 +543,21 @@ def query(dom, query, context=None):
:param context: Context Node
:type: DOMElement
+ :param tagid: Tag ID
+ :type: string
+
:returns: The queried nodes
:rtype: list
"""
if context is None:
- return dom.xpath(query, namespaces=OneLogin_Saml2_Constants.NSMAP)
+ source = dom
+ else:
+ source = context
+
+ if tagid is None:
+ return source.xpath(query, namespaces=OneLogin_Saml2_Constants.NSMAP)
else:
- return context.xpath(query, namespaces=OneLogin_Saml2_Constants.NSMAP)
+ return source.xpath(query, tagid=tagid, namespaces=OneLogin_Saml2_Constants.NSMAP)
@staticmethod
def delete_local_session(callback=None):
@@ -668,7 +676,7 @@ def generate_name_id(value, sp_nq, sp_format=None, cert=None, debug=False, nq=No
if cert is not None:
xml = name_id_container.toxml()
- elem = fromstring(xml)
+ elem = fromstring(xml, forbid_dtd=True)
error_callback_method = None
if debug:
@@ -697,7 +705,7 @@ def generate_name_id(value, sp_nq, sp_format=None, cert=None, debug=False, nq=No
edata = enc_ctx.encryptXml(enc_data, elem[0])
- newdoc = parseString(tostring(edata, encoding='unicode').encode('utf-8'))
+ newdoc = parseString(tostring(edata, encoding='unicode').encode('utf-8'), forbid_dtd=True)
if newdoc.hasChildNodes():
child = newdoc.firstChild
@@ -783,9 +791,9 @@ def decrypt_element(encrypted_data, key, debug=False, inplace=False):
:rtype: lxml.etree.Element
"""
if isinstance(encrypted_data, Element):
- encrypted_data = fromstring(str(encrypted_data.toxml()))
+ encrypted_data = fromstring(str(encrypted_data.toxml()), forbid_dtd=True)
elif isinstance(encrypted_data, basestring):
- encrypted_data = fromstring(str(encrypted_data))
+ encrypted_data = fromstring(str(encrypted_data), forbid_dtd=True)
elif not inplace and isinstance(encrypted_data, etree._Element):
encrypted_data = deepcopy(encrypted_data)
@@ -851,7 +859,7 @@ def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constant
elem = xml
elif isinstance(xml, Document):
xml = xml.toxml()
- elem = fromstring(xml.encode('utf-8'))
+ elem = fromstring(xml.encode('utf-8'), forbid_dtd=True)
elif isinstance(xml, Element):
xml.setAttributeNS(
unicode(OneLogin_Saml2_Constants.NS_SAMLP),
@@ -864,9 +872,9 @@ def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constant
unicode(OneLogin_Saml2_Constants.NS_SAML)
)
xml = xml.toxml()
- elem = fromstring(xml.encode('utf-8'))
+ elem = fromstring(xml.encode('utf-8'), forbid_dtd=True)
elif isinstance(xml, basestring):
- elem = fromstring(xml.encode('utf-8'))
+ elem = fromstring(xml.encode('utf-8'), forbid_dtd=True)
else:
raise Exception('Error parsing xml string')
@@ -939,8 +947,6 @@ def add_sign(xml, key, cert, debug=False, sign_algorithm=OneLogin_Saml2_Constant
dsig_ctx.sign(signature)
return tostring(elem, encoding='unicode').encode('utf-8')
- newdoc = parseString(tostring(elem, encoding='unicode').encode('utf-8'))
- return newdoc.saveXML(newdoc.firstChild)
@staticmethod
@return_false_on_exception
@@ -981,7 +987,7 @@ def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', valid
elem = xml
elif isinstance(xml, Document):
xml = xml.toxml()
- elem = fromstring(str(xml))
+ elem = fromstring(str(xml), forbid_dtd=True)
elif isinstance(xml, Element):
xml.setAttributeNS(
unicode(OneLogin_Saml2_Constants.NS_SAMLP),
@@ -994,9 +1000,9 @@ def validate_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha1', valid
unicode(OneLogin_Saml2_Constants.NS_SAML)
)
xml = xml.toxml()
- elem = fromstring(str(xml))
+ elem = fromstring(str(xml), forbid_dtd=True)
elif isinstance(xml, basestring):
- elem = fromstring(str(xml))
+ elem = fromstring(str(xml), forbid_dtd=True)
else:
raise Exception('Error parsing xml string')
@@ -1065,7 +1071,7 @@ def validate_metadata_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha
elem = xml
elif isinstance(xml, Document):
xml = xml.toxml()
- elem = fromstring(str(xml))
+ elem = fromstring(str(xml), forbid_dtd=True)
elif isinstance(xml, Element):
xml.setAttributeNS(
unicode(OneLogin_Saml2_Constants.NS_MD),
@@ -1073,9 +1079,9 @@ def validate_metadata_sign(xml, cert=None, fingerprint=None, fingerprintalg='sha
unicode(OneLogin_Saml2_Constants.NS_MD)
)
xml = xml.toxml()
- elem = fromstring(str(xml))
+ elem = fromstring(str(xml), forbid_dtd=True)
elif isinstance(xml, basestring):
- elem = fromstring(str(xml))
+ elem = fromstring(str(xml), forbid_dtd=True)
else:
raise Exception('Error parsing xml string')
From c999587f8b0cafd43a16ef5fa16dede6b9dfee93 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Tue, 29 Jan 2019 13:31:10 +0100
Subject: [PATCH 163/255] flake8 fixes
---
src/onelogin/saml2/logout_request.py | 2 +-
src/onelogin/saml2/logout_response.py | 2 +-
src/onelogin/saml2/response.py | 2 +-
src/onelogin/saml2/utils.py | 2 +-
tests/src/OneLogin/saml2_tests/utils_test.py | 6 ++++--
5 files changed, 8 insertions(+), 6 deletions(-)
diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py
index f201ff36..e419f585 100644
--- a/src/onelogin/saml2/logout_request.py
+++ b/src/onelogin/saml2/logout_request.py
@@ -425,7 +425,7 @@ def is_valid(self, request_data, raise_exceptions=False):
self.__error = err.__str__()
debug = self.__settings.is_debug_active()
if debug:
- print err.__str__()
+ print(err.__str__())
if raise_exceptions:
raise err
return False
diff --git a/src/onelogin/saml2/logout_response.py b/src/onelogin/saml2/logout_response.py
index 3e6be91f..4af91a4f 100644
--- a/src/onelogin/saml2/logout_response.py
+++ b/src/onelogin/saml2/logout_response.py
@@ -185,7 +185,7 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False):
self.__error = err.__str__()
debug = self.__settings.is_debug_active()
if debug:
- print err.__str__()
+ print(err.__str__())
if raise_exceptions:
raise err
return False
diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py
index da9c1d77..798f51bf 100644
--- a/src/onelogin/saml2/response.py
+++ b/src/onelogin/saml2/response.py
@@ -328,7 +328,7 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False):
self.__error = err.__str__()
debug = self.__settings.is_debug_active()
if debug:
- print err.__str__()
+ print(err.__str__())
if raise_exceptions:
raise err
return False
diff --git a/src/onelogin/saml2/utils.py b/src/onelogin/saml2/utils.py
index 8d11447a..8fea81d1 100644
--- a/src/onelogin/saml2/utils.py
+++ b/src/onelogin/saml2/utils.py
@@ -75,7 +75,7 @@ def print_xmlsec_errors(filename, line, func, error_object, error_subject, reaso
if reason != 1:
info.append("errno=%d" % reason)
if info:
- print "%s:%d(%s)" % (filename, line, func), " ".join(info)
+ print("%s:%d(%s)" % (filename, line, func), " ".join(info))
class OneLogin_Saml2_Utils(object):
diff --git a/tests/src/OneLogin/saml2_tests/utils_test.py b/tests/src/OneLogin/saml2_tests/utils_test.py
index d0c73e86..9fa38952 100644
--- a/tests/src/OneLogin/saml2_tests/utils_test.py
+++ b/tests/src/OneLogin/saml2_tests/utils_test.py
@@ -655,6 +655,9 @@ def testCalculateX509Fingerprint(self):
self.assertEqual('3db29251b97559c67988ea0754cb0573fc409b6f75d89282d57cfb75089539b0bbdb2dcd9ec6e032549ecbc466439d5992e18db2cf5494ca2fe1b2e16f348dff', OneLogin_Saml2_Utils.calculate_x509_fingerprint(cert, 'sha512'))
+ def dscb(self):
+ return self.session_clear()
+
def testDeleteLocalSession(self):
"""
Tests the delete_local_session method of the OneLogin_Saml2_Utils
@@ -665,8 +668,7 @@ def testDeleteLocalSession(self):
OneLogin_Saml2_Utils.delete_local_session()
self.assertEqual(1, local_session_test)
- dscb = lambda: self.session_clear()
- OneLogin_Saml2_Utils.delete_local_session(dscb)
+ OneLogin_Saml2_Utils.delete_local_session(self.dscb)
self.assertEqual(0, local_session_test)
def session_clear(self):
From 44a92d35a8027b7e396d4f3f5257beeefeffce0e Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Tue, 29 Jan 2019 17:36:41 +0100
Subject: [PATCH 164/255] Fix #239 .Check that the response has all of the
AuthnContexts that we provided
---
src/onelogin/saml2/auth.py | 2 +
src/onelogin/saml2/errors.py | 1 +
src/onelogin/saml2/response.py | 22 +++++++++++
src/onelogin/saml2/settings.py | 1 +
.../src/OneLogin/saml2_tests/response_test.py | 38 +++++++++++++++++++
5 files changed, 64 insertions(+)
diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py
index eb61705e..fa5cb243 100644
--- a/src/onelogin/saml2/auth.py
+++ b/src/onelogin/saml2/auth.py
@@ -62,6 +62,7 @@ def __init__(self, request_data, old_settings=None, custom_base_path=None):
self.__last_message_id = None
self.__last_assertion_id = None
self.__last_assertion_not_on_or_after = None
+ self.__last_authn_contexts = []
self.__last_request = None
self.__last_response = None
@@ -107,6 +108,7 @@ def process_response(self, request_id=None):
self.__session_expiration = response.get_session_not_on_or_after()
self.__last_message_id = response.get_id()
self.__last_assertion_id = response.get_assertion_id()
+ self.__last_authn_contexts = response.get_authn_contexts()
self.__last_assertion_not_on_or_after = response.get_assertion_not_on_or_after()
self.__authenticated = True
diff --git a/src/onelogin/saml2/errors.py b/src/onelogin/saml2/errors.py
index ec879bfd..560ec8e6 100644
--- a/src/onelogin/saml2/errors.py
+++ b/src/onelogin/saml2/errors.py
@@ -112,6 +112,7 @@ class OneLogin_Saml2_ValidationError(Exception):
INVALID_SIGNATURE = 42
WRONG_NUMBER_OF_SIGNATURES = 43
RESPONSE_EXPIRED = 44
+ AUTHN_CONTEXT_MISMATCH = 45
def __init__(self, message, code=0, errors=None):
"""
diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py
index 798f51bf..0ce801ea 100644
--- a/src/onelogin/saml2/response.py
+++ b/src/onelogin/saml2/response.py
@@ -178,6 +178,19 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False):
OneLogin_Saml2_ValidationError.WRONG_NUMBER_OF_AUTHSTATEMENTS
)
+ # Checks that the response has all of the AuthnContexts that we provided in the request.
+ # Only check if failOnAuthnContextMismatch is true and requestedAuthnContext is set to a list.
+ requested_authn_contexts = security.get('requestedAuthnContext', True)
+
+ if security.get('failOnAuthnContextMismatch', False) and requested_authn_contexts and requested_authn_contexts is not True:
+ authn_contexts = self.get_authn_contexts()
+ unmatched_contexts = set(requested_authn_contexts).difference(authn_contexts)
+ if unmatched_contexts:
+ raise OneLogin_Saml2_ValidationError(
+ 'The AuthnContext "%s" didn\'t include requested context "%s"' % (', '.join(authn_contexts), ', '.join(unmatched_contexts)),
+ OneLogin_Saml2_ValidationError.AUTHN_CONTEXT_MISMATCH
+ )
+
# Checks that there is at least one AttributeStatement if required
attribute_statement_nodes = self.__query_assertion('/saml:AttributeStatement')
if security.get('wantAttributeStatement', True) and not attribute_statement_nodes:
@@ -383,6 +396,15 @@ def get_audiences(self):
audience_nodes = self.__query_assertion('/saml:Conditions/saml:AudienceRestriction/saml:Audience')
return [OneLogin_Saml2_Utils.element_text(node) for node in audience_nodes if OneLogin_Saml2_Utils.element_text(node) is not None]
+ def get_authn_contexts(self):
+ """
+ Gets the authentication contexts
+ :returns: The authentication classes for the SAML Response
+ :rtype: list
+ """
+ authn_context_nodes = self.__query_assertion('/saml:AuthnStatement/saml:AuthnContext/saml:AuthnContextClassRef')
+ return [OneLogin_Saml2_Utils.element_text(node) for node in authn_context_nodes]
+
def get_issuers(self):
"""
Gets the issuers (from message and from assertion)
diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py
index f3c53be1..c210c5b3 100644
--- a/src/onelogin/saml2/settings.py
+++ b/src/onelogin/saml2/settings.py
@@ -304,6 +304,7 @@ def __add_default_values(self):
self.__sp.setdefault('privateKey', '')
self.__security.setdefault('requestedAuthnContext', True)
+ self.__security.setdefault('failOnAuthnContextMismatch', False)
def check_settings(self, settings):
"""
diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py
index d6daaf97..bc172546 100644
--- a/tests/src/OneLogin/saml2_tests/response_test.py
+++ b/tests/src/OneLogin/saml2_tests/response_test.py
@@ -980,6 +980,44 @@ def testIsInValidAudience(self):
self.assertFalse(response_2.is_valid(request_data))
self.assertIn('is not a valid audience for this Response', response_2.get_error())
+ def testIsInValidAuthenticationContext(self):
+ """
+ Tests that requestedAuthnContext, when set, is compared against the
+ response AuthnContext, which is what you use for two-factor
+ authentication. Without this check you can get back a valid response
+ that didn't complete the two-factor step.
+ """
+ request_data = self.get_request_data()
+ message = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64'))
+ two_factor_context = 'urn:oasis:names:tc:SAML:2.0:ac:classes:TimeSyncToken'
+ password_context = 'urn:oasis:names:tc:SAML:2.0:ac:classes:Password'
+ settings_dict = self.loadSettingsJSON()
+ settings_dict['security']['requestedAuthnContext'] = [two_factor_context]
+ settings_dict['security']['failOnAuthnContextMismatch'] = True
+ settings_dict['strict'] = True
+ settings = OneLogin_Saml2_Settings(settings_dict)
+
+ # check that we catch when the contexts don't match
+ response = OneLogin_Saml2_Response(settings, message)
+ self.assertFalse(response.is_valid(request_data))
+ self.assertIn('The AuthnContext "%s" didn\'t include requested context "%s"' % (password_context, two_factor_context), response.get_error())
+
+ # now drop in the expected AuthnContextClassRef and see that it passes
+ original_message = b64decode(message)
+ two_factor_message = original_message.replace(password_context, two_factor_context)
+ two_factor_message = b64encode(two_factor_message)
+ response = OneLogin_Saml2_Response(settings, two_factor_message)
+ response.is_valid(request_data)
+ # check that we got as far as destination validation, which comes later
+ self.assertIn('The response was received at', response.get_error())
+
+ # with the default setting, check that we succeed with our original context
+ settings_dict['security']['requestedAuthnContext'] = True
+ settings = OneLogin_Saml2_Settings(settings_dict)
+ response = OneLogin_Saml2_Response(settings, message)
+ response.is_valid(request_data)
+ self.assertIn('The response was received at', response.get_error())
+
def testIsInValidIssuer(self):
"""
Tests the is_valid method of the OneLogin_Saml2_Response class
From f7d7034b92bb57cef4bbcd7858c7c78b13dbc862 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Tue, 29 Jan 2019 17:37:45 +0100
Subject: [PATCH 165/255] Release 2.5.0
---
README.md | 5 +++++
changelog.md | 10 ++++++++++
setup.py | 2 +-
3 files changed, 16 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 64d01d1d..971eeea1 100644
--- a/README.md
+++ b/README.md
@@ -14,6 +14,8 @@ Python3: [python3-saml](https://github.com/onelogin/python3-saml).
#### Warning ####
+Update python-saml to 2.5.0, this version includes security improvements for preventing XEE and Xpath Injections.
+
Update python-saml to 2.4.0, this version includes a fix for the [CVE-2017-11427](https://www.cvedetails.com/cve/CVE-2017-11427/) vulnerability.
This version also changes how the calculate fingerprint method works, and will expect as input a formatted x509 certificate
@@ -436,6 +438,9 @@ In addition to the required settings data (idp, sp), extra settings can be defin
// Allows the authn comparison parameter to be set, defaults to 'exact' if the setting is not present.
"requestedAuthnContextComparison": "exact",
+ // Set to true to check that the AuthnContext received matches the one requested.
+ "failOnAuthnContextMismatch": false,
+
// In some environment you will need to set how long the published metadata of the Service Provider gonna be valid.
// is possible to not set the 2 following parameters (or set to null) and default values will be set (2 days, 1 week)
// Provide the desired Timestamp, for example 2015-06-26T20:00:00Z
diff --git a/changelog.md b/changelog.md
index 996779c1..91de981e 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,4 +1,14 @@
# python-saml changelog
+### 2.5.0 (Jan 29, 2019)
+* Security improvements. Use of tagid to prevent XPath injection. Disable DTD on fromstring defusedxml method
+* [#239](https://github.com/onelogin/python-saml/issues/239) Check that the response has all of the AuthnContexts that we provided
+* Fixed a ValidationError misspelling
+* Don't require compression on LogoutResponse messages by relaxing the decode_base64_and_inflate method
+* Add expected/received in WRONG_ISSUER error
+* If debug enable, print reason for the SAMLResponse invalidation
+* [#238](https://github.com/onelogin/python-saml/issues/238) Fix DSA constant
+* Start using flake8 for code quality
+
### 2.4.2 (Sep 05, 2018)
* Update dm.xmlsec.binding dependency to 1.3.7
* Update pylint dependency to 1.9.1
diff --git a/setup.py b/setup.py
index c3f16089..be6c5d69 100644
--- a/setup.py
+++ b/setup.py
@@ -9,7 +9,7 @@
setup(
name='python-saml',
- version='2.4.2',
+ version='2.5.0',
description='Onelogin Python Toolkit. Add SAML support to your Python software using this library',
classifiers=[
'Development Status :: 5 - Production/Stable',
From 4f7add5b2c1491038048eed529b949159d1db7f4 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Wed, 30 Jan 2019 16:58:34 +0100
Subject: [PATCH 166/255] Fix bug on friendlyName/nameFormat parameters on
RequestedAttribute elements. Wrong variable name caused FriendlyName to
overwrite NameFormat
---
src/onelogin/saml2/metadata.py | 2 +-
tests/src/OneLogin/saml2_tests/metadata_test.py | 12 ++++++------
2 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/src/onelogin/saml2/metadata.py b/src/onelogin/saml2/metadata.py
index 9a931c9e..2260bb8b 100644
--- a/src/onelogin/saml2/metadata.py
+++ b/src/onelogin/saml2/metadata.py
@@ -91,7 +91,7 @@ def builder(sp, authnsign=False, wsign=False, valid_until=None, cache_duration=N
if 'nameFormat' in req_attribs.keys() and req_attribs['nameFormat']:
req_attr_nameformat_str = " NameFormat=\"%s\"" % req_attribs['nameFormat']
if 'friendlyName' in req_attribs.keys() and req_attribs['friendlyName']:
- req_attr_nameformat_str = " FriendlyName=\"%s\"" % req_attribs['friendlyName']
+ req_attr_friendlyname_str = " FriendlyName=\"%s\"" % req_attribs['friendlyName']
if 'isRequired' in req_attribs.keys() and req_attribs['isRequired']:
req_attr_isrequired_str = " isRequired=\"%s\"" % 'true' if req_attribs['isRequired'] else 'false'
diff --git a/tests/src/OneLogin/saml2_tests/metadata_test.py b/tests/src/OneLogin/saml2_tests/metadata_test.py
index a63a552e..60453026 100644
--- a/tests/src/OneLogin/saml2_tests/metadata_test.py
+++ b/tests/src/OneLogin/saml2_tests/metadata_test.py
@@ -158,11 +158,11 @@ def testBuilderAttributeConsumingService(self):
self.assertIn("""
Test Service
Test Service
-
-
-
-
-
+
+
+
+
+
""", metadata)
def testBuilderAttributeConsumingServiceWithMultipleAttributeValue(self):
@@ -184,7 +184,7 @@ def testBuilderAttributeConsumingServiceWithMultipleAttributeValue(self):
userType
admin
-
+
""", metadata)
def testSignMetadata(self):
From cd97e1f40bb7a04abebb2e846fff0cc53d144461 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Thu, 31 Jan 2019 12:27:33 +0100
Subject: [PATCH 167/255] Add get_last_authn_contexts method
---
README.md | 1 +
src/onelogin/saml2/auth.py | 7 +++++++
tests/src/OneLogin/saml2_tests/auth_test.py | 13 +++++++++++++
3 files changed, 21 insertions(+)
diff --git a/README.md b/README.md
index 971eeea1..ae669920 100644
--- a/README.md
+++ b/README.md
@@ -940,6 +940,7 @@ Main class of OneLogin Python Toolkit
* ***get_sso_url*** Gets the SSO url.
* ***get_slo_url*** Gets the SLO url.
* ***get_last_request_id*** The ID of the last Request SAML message generated (AuthNRequest, LogoutRequest).
+* ***get_last_authn_contexts*** Returns the list of authentication contexts sent in the last SAML Response.
* ***build_request_signature*** Builds the Signature of the SAML Request.
* ***build_response_signature*** Builds the Signature of the SAML Response.
* ***get_settings*** Returns the settings info.
diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py
index fa5cb243..722c1c22 100644
--- a/src/onelogin/saml2/auth.py
+++ b/src/onelogin/saml2/auth.py
@@ -202,6 +202,13 @@ def redirect_to(self, url=None, parameters={}):
url = self.__request_data['get_data']['RelayState']
return OneLogin_Saml2_Utils.redirect(url, parameters, request_data=self.__request_data)
+ def get_last_authn_contexts(self):
+ """
+ :returns: The list of authentication contexts sent in the last SAML Response.
+ :rtype: list
+ """
+ return self.__last_authn_contexts
+
def is_authenticated(self):
"""
Checks if the user is authenticated or not.
diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py
index 2b946553..3c36d5a4 100644
--- a/tests/src/OneLogin/saml2_tests/auth_test.py
+++ b/tests/src/OneLogin/saml2_tests/auth_test.py
@@ -1053,6 +1053,19 @@ def testGetLastAuthnRequest(self):
)
self.assertIn(expectedFragment, auth.get_last_request_xml())
+ def testGetLastAuthnContexts(self):
+ settings = self.loadSettingsJSON()
+ request_data = self.get_request()
+ message = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64'))
+ del request_data['get_data']
+ request_data['post_data'] = {
+ 'SAMLResponse': message
+ }
+ auth = OneLogin_Saml2_Auth(request_data, old_settings=settings)
+
+ auth.process_response()
+ self.assertEqual(auth.get_last_authn_contexts(), ['urn:oasis:names:tc:SAML:2.0:ac:classes:Password'])
+
def testGetLastLogoutRequest(self):
settings = self.loadSettingsJSON()
auth = OneLogin_Saml2_Auth({'http_host': 'localhost', 'script_name': 'thing'}, old_settings=settings)
From c789f1547647ce3fc289b1291e342b54104d6982 Mon Sep 17 00:00:00 2001
From: Victor Mireyev
Date: Tue, 26 Feb 2019 23:33:07 +0300
Subject: [PATCH 168/255] Fix typo in docstring.
---
src/onelogin/saml2/authn_request.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/onelogin/saml2/authn_request.py b/src/onelogin/saml2/authn_request.py
index d0983703..009c7233 100644
--- a/src/onelogin/saml2/authn_request.py
+++ b/src/onelogin/saml2/authn_request.py
@@ -27,7 +27,7 @@ def __init__(self, settings, force_authn=False, is_passive=False, set_nameid_pol
Constructs the AuthnRequest object.
:param settings: OSetting data
- :type return_to: OneLogin_Saml2_Settings
+ :type settings: OneLogin_Saml2_Settings
:param force_authn: Optional argument. When true the AuthNRequest will set the ForceAuthn='true'.
:type force_authn: bool
From 8a98992dd016d256a99aae0eee9e608dbc2d0835 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Tue, 2 Apr 2019 13:24:39 +0200
Subject: [PATCH 169/255] Add support for Subjects on AuthNRequests by the new
name_id_value_req parameeter.Fix testshib test. Improve README: Added inline
markup to important references
---
README.md | 299 +++++++++---------
src/onelogin/saml2/auth.py | 7 +-
src/onelogin/saml2/authn_request.py | 16 +-
tests/data/metadata/testshib-providers.xml | 39 +--
tests/src/OneLogin/saml2_tests/auth_test.py | 47 ++-
.../saml2_tests/authn_request_test.py | 32 ++
6 files changed, 262 insertions(+), 178 deletions(-)
diff --git a/README.md b/README.md
index ae669920..3f70a5b1 100644
--- a/README.md
+++ b/README.md
@@ -14,17 +14,17 @@ Python3: [python3-saml](https://github.com/onelogin/python3-saml).
#### Warning ####
-Update python-saml to 2.5.0, this version includes security improvements for preventing XEE and Xpath Injections.
+Update ``python-saml`` to ``2.5.0``, this version includes security improvements for preventing XEE and Xpath Injections.
-Update python-saml to 2.4.0, this version includes a fix for the [CVE-2017-11427](https://www.cvedetails.com/cve/CVE-2017-11427/) vulnerability.
+Update ``python-saml`` to ``2.4.0``, this version includes a fix for the [CVE-2017-11427](https://www.cvedetails.com/cve/CVE-2017-11427/) vulnerability.
-This version also changes how the calculate fingerprint method works, and will expect as input a formatted x509 certificate
+This version also changes how the calculate fingerprint method works, and will expect as input a formatted X.509 certificate
-Update python-saml to 2.2.3, this version replaces some etree.tostring calls, that were introduced recently, by the sanitized call provided by defusedxml
+Update ``python-saml`` to ``2.2.3``, this version replaces some etree.tostring calls, that were introduced recently, by the sanitized call provided by ``defusedxml``
-Update python-saml to 2.2.0, this version includes a security patch that contains extra validations that will prevent signature wrapping attacks. [CVE-2016-1000252](https://github.com/distributedweaknessfiling/DWF-Database-Artifacts/blob/master/DWF/2016/1000252/CVE-2016-1000252.json)
+Update ``python-saml`` to ``2.2.0``, this version includes a security patch that contains extra validations that will prevent signature wrapping attacks. [CVE-2016-1000252](https://github.com/distributedweaknessfiling/DWF-Database-Artifacts/blob/master/DWF/2016/1000252/CVE-2016-1000252.json)
-python-saml < v2.2.0 is vulnerable and allows signature wrapping!
+``python-saml`` < ``v2.2.0`` is vulnerable and allows signature wrapping!
#### Security Guidelines ####
@@ -53,23 +53,23 @@ since 2002, but lately it is becoming popular due its advantages:
* **Opportunity** - B2B cloud vendor should support SAML to facilitate the
integration of their product.
-General description
+General Description
-------------------
OneLogin's SAML Python toolkit lets you turn your Python application into a SP
(Service Provider) that can be connected to an IdP (Identity Provider).
-Supports:
+**Supports:**
* SSO and SLO (SP-Initiated and IdP-Initiated).
* Assertion and nameId encryption.
* Assertion signatures.
- * Message signatures: AuthNRequest, LogoutRequest, LogoutResponses.
+ * Message signatures: ``AuthNRequest``, ``LogoutRequest``, ``LogoutResponses``.
* Enable an Assertion Consumer Service endpoint.
* Enable a Single Logout Service endpoint.
* Publish the SP metadata (which can be signed).
-Key features:
+**Key features:**
* **saml2int** - Implements the SAML 2.0 Web Browser SSO Profile.
* **Session-less** - Forget those common conflicts between the SP and
@@ -77,7 +77,7 @@ Key features:
* **Easy to use** - Programmer will be allowed to code high-level and
low-level programming, 2 easy to use APIs are available.
* **Tested** - Thoroughly tested.
- * **Popular** - OneLogin's customers use it. Add easy support to your django/flask/bottle web projects.
+ * **Popular** - OneLogin's customers use it. Add easy support to your Django/Flask/Bottle/Pyramid web projects.
Installation
@@ -106,18 +106,18 @@ $ brew install libxmlsec1
### Code ###
-#### Option 1. Download from github ####
+#### Option 1. Download from Github ####
-The toolkit is hosted on github. You can download it from:
+The toolkit is hosted on Github. You can download it from:
* Lastest release: https://github.com/onelogin/python-saml/releases/latest
* Master repo: https://github.com/onelogin/python-saml/tree/master
-Copy the core of the library (src/onelogin/saml2 folder) and merge the setup.py inside the python application. (each application has its structure so take your time to locate the Python SAML toolkit in the best place).
+Copy the core of the library ``(src/onelogin/saml2 folder)`` and merge the setup.py inside the Python application. (Each application has its structure so take your time to locate the Python SAML toolkit in the best place).
#### Option 2. Download from pypi ####
-The toolkit is hosted in pypi, you can find the python-saml package at https://pypi.python.org/pypi/python-saml
+The toolkit is hosted in pypi, you can find the ``python-saml`` package at https://pypi.python.org/pypi/python-saml
You can install it executing:
```
@@ -127,7 +127,7 @@ $ pip install python-saml
If you want to know how a project can handle python packages review this [guide](https://packaging.python.org/en/latest/tutorial.html) and review this [sampleproject](https://github.com/pypa/sampleproject)
-Security warning
+Security Warning
----------------
In production, the **strict** parameter MUST be set as **"true"**. Otherwise
@@ -136,12 +136,12 @@ your environment is not secure and will be exposed to attacks.
In production also we highly recommend to register on the settings the IdP certificate instead of using the fingerprint method. The fingerprint, is a hash, so at the end is open to a collision attack that can end on a signature validation bypass. Other SAML toolkits deprecated that mechanism, we maintain it for compatibility and also to be used on test environment.
-Getting started
+Getting Started
---------------
### Knowing the toolkit ###
-The new OneLogin SAML Toolkit contains different folders (certs, lib, demo-django, demo-flask, demo-bottle and tests) and some files.
+The new OneLogin SAML Toolkit contains different folders (``cert``, ``lib``, ``demo-django``, ``demo-flask``, ``demo-bottle`` and ``tests``) and some files.
Let's start describing them:
@@ -152,23 +152,23 @@ the classes and methods that are described in a later section.
#### demo-django ####
-This folder contains a Django project that will be used as demo to show how to add SAML support to the Django Framework. 'demo' is the main folder of the django project (with its settings.py, views.py, urls.py), 'templates' is the django templates of the project and 'saml' is a folder that contains the 'certs' folder that could be used to store the x509 public and private key, and the saml toolkit settings (settings.json and advanced_settings.json).
+This folder contains a Django project that will be used as demo to show how to add SAML support to the Django Framework. **demo** is the main folder of the Django project (with its ``settings.py``, ``views.py``, ``urls.py``), **templates** is the Django templates of the project and **saml** is a folder that contains the 'certs' folder that could be used to store the X.509 public and private key, and the SAML toolkit settings (``settings.json`` and ``advanced_settings.json``).
*** Notice about certs ***
-SAML requires a x.509 cert to sign and encrypt elements like NameID, Message, Assertion, Metadata.
+SAML requires a x.509 cert to sign and encrypt elements like ``NameID``, ``Message``, ``Assertion``, ``Metadata``.
-If our environment requires sign or encrypt support, the certs folder may contain the x509 cert and the private key that the SP will use:
+If our environment requires sign or encrypt support, the certs folder may contain the X.509 cert and the private key that the SP will use:
* 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 'x509cert' 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.
+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``.
-Use `sp_new.crt` if you are in a key rollover process and you want to
-publish that x509certificate on Service Provider metadata.
+Use ``sp_new.crt`` if you are in a key rollover process and you want to
+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:
@@ -178,17 +178,17 @@ openssl req -new -x509 -days 3652 -nodes -out sp.crt -keyout saml.key
#### demo-bottle ####
-This folder contains a Bottle project that will be used as demo to show how to add SAML support to the Bottle Framework. index.py contains all the logic of the demo project, 'templates' is the Bottle templates of the project and 'saml' is a folder that contains the 'certs' folder that could be used to store the x509 public and private key, and the saml toolkit settings (settings.json and advanced_settings.json).
+This folder contains a Bottle project that will be used as demo to show how to add SAML support to the Bottle Framework. ``index.py`` contains all the logic of the demo project, **templates** is the Bottle templates of the project and **saml** is a folder that contains the 'certs' folder that could be used to store the X.509 public and private key, and the SAML toolkit settings (``settings.json`` and ``advanced_settings.json``).
#### demo-flask ####
-This folder contains a Flask project that will be used as demo to show how to add SAML support to the Flask Framework. 'index.py' is the main flask file that has all the code, this file uses the templates stored at the 'templates' folder. In the 'saml' folder we found the 'certs' folder to store the x509 public and private key, and the saml toolkit settings (settings.json and advanced_settings.json).
+This folder contains a Flask project that will be used as demo to show how to add SAML support to the Flask Framework. ``index.py`` is the main Flask file that has all the code, this file uses the templates stored at the 'templates' folder. In the 'saml' folder we found the 'certs' folder to store the X.509 public and private key, and the SAML toolkit settings (``settings.json`` and ``advanced_settings.json``).
#### demo_pyramid ####
-This folder contains a Pyramid project that will be used as demo to show how to add SAML support to the [Pyramid Web Framework](http://docs.pylonsproject.org/projects/pyramid/en/latest/). '\_\_init__.py' is the main file that configures the app and its routes, 'views.py' is where all the logic and SAML handling takes place, and the templates are stored in the 'templates' folder. The 'saml' folder is the same as in the other two demos.
+This folder contains a Pyramid project that will be used as demo to show how to add SAML support to the [Pyramid Web Framework](http://docs.pylonsproject.org/projects/pyramid/en/latest/). ``\_\_init__.py`` is the main file that configures the app and its routes, ``views.py`` is where all the logic and SAML handling takes place, and the templates are stored in the **templates** folder. The **saml** folder is the same as in the other two demos.
#### setup.py ####
@@ -200,7 +200,7 @@ Read more at https://pythonhosted.org/an_example_pypi_project/setuptools.html
Contains the unit test of the toolkit.
-In order to execute the test you need to load the virtualenv with the toolkit installed on it and execute:
+In order to execute the test you need to load the ``virtualenv`` with the toolkit installed on it and execute:
```
pip install -e ".[test]"
```
@@ -215,9 +215,9 @@ The previous line will run the tests for the whole toolkit. You can also run the
python setup.py test --test-suite tests.src.OneLogin.saml2_tests.auth_test.OneLogin_Saml2_Auth_Test
```
-With the --test-suite parameter you can specify the module to test. You'll find all the module available and their class names at tests/src/OneLogin/saml2_tests/
+With the ``--test-suite`` parameter you can specify the module to test. You'll find all the module available and their class names at ``tests/src/OneLogin/saml2_tests/``
-### How it works ###
+### How it Works ###
#### Settings ####
@@ -225,13 +225,13 @@ First of all we need to configure the toolkit. The SP's info, the IdP's info, an
There are two ways to provide the settings information:
-* Use a settings.json file that we should locate in any folder, but indicates its path with the 'custom_base_path' parameter.
+* Use a ``settings.json`` file that we should locate in any folder, but indicates its path with the ``custom_base_path`` parameter.
-* Use a json object with the setting data and provide it directly to the constructor of the class (if your toolkit integation requires certs, remember to provide the 'custom_base_path' as part of the settings or as a parameter in the constructor.
+* Use a JSON object with the setting data and provide it directly to the constructor of the class (if your toolkit integation requires certs, remember to provide the ``custom_base_path`` as part of the settings or as a parameter in the constructor.
-In the demo-django, demo-flask and demo-bottle folders you will find a 'saml' folder, inside there is a 'certs' folder and a settings.json and a advanced_settings.json files. Those files contain the settings for the saml toolkit. Copy them in your project and set the correct values.
+In the ``demo-django``, ``demo-flask``, ``demo-pyramid`` and ``demo-bottle`` folders you will find a ``saml`` folder, inside there is a ``certs`` folder and a ``settings.json`` and a ``advanced_settings.json`` files. Those files contain the settings for the SAML toolkit. Copy them in your project and set the correct values.
-This is the settings.json file:
+This is the ``settings.json`` file:
```javascript
{
@@ -288,15 +288,15 @@ 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 x509cert 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": ""
/*
* Key rollover
- * If you plan to update the SP x509cert and privateKey
- * you can define here the new x509cert and it will be
+ * If you plan to update the SP X.509 cert and privateKey
+ * you can define here the new X.509 cert and it will be
* published on the SP metadata so Identity Providers can
* read them and get ready for rollover.
*/
@@ -326,11 +326,11 @@ This is the settings.json file:
// only for this endpoint.
"binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
},
- // Public x509 certificate of the IdP
+ // Public X.509 certificate of the IdP
"x509cert": ""
/*
- * Instead of using the whole x509cert you can use a fingerprint in order to
- * validate a SAMLResponse (but you still need the x509cert to validate LogoutRequest and LogoutResponse using the HTTP-Redirect binding).
+ * Instead of using the whole X.509 cert you can use a fingerprint in order to
+ * validate a SAMLResponse (but you still need the X.509 cert to validate LogoutRequest and LogoutResponse using the HTTP-Redirect binding).
* 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.
*
@@ -342,7 +342,7 @@ This is the settings.json file:
* 'sha1' is the default value.
*
* Notice that if you want to validate any SAML Message sent by the HTTP-Redirect binding, you
- * will need to provide the whole x509cert.
+ * will need to provide the whole X.509 cert.
*
*/
// 'certFingerprint': '',
@@ -367,7 +367,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
{
@@ -489,11 +489,11 @@ In addition to the required settings data (idp, sp), extra settings can be defin
}
```
-In the security section, you can set the way that the SP will handle the messages and assertions. Contact the admin of the IdP and ask them what the IdP expects, and decide what validations will handle the SP and what requirements the SP will have and communicate them to the IdP's admin too.
+In the ``security`` section, you can set the way that the SP will handle the messages and assertions. Contact the admin of the IdP and ask them what the IdP expects, and decide what validations will handle the SP and what requirements the SP will have and communicate them to the IdP's admin too.
Once we know what kind of data could be configured, let's talk about the way settings are handled within the toolkit.
-The settings files described (settings.json and advanced_settings.json) are loaded by the toolkit if not other dict with settings info is provided in the constructors of the toolkit. Let's see some examples.
+The settings files described (``settings.json`` and ``advanced_settings.json``) are loaded by the toolkit if not other dict with settings info is provided in the constructors of the toolkit. Let's see some examples.
```python
# Initializes toolkit with settings.json & advanced_settings.json files.
@@ -513,7 +513,7 @@ auth = OneLogin_Saml2_Auth(req, settings_data)
settings = OneLogin_Saml2_Settings(settings_data)
```
-You can declare the settings_data in the file that constains the constructor execution or locate them in any file and load the file in order to get the dict available as we see in the following example:
+You can declare the ``settings_data`` in the file that constains the constructor execution or locate them in any file and load the file in order to get the dict available as we see in the following example:
```python
filename = "/var/www/django-project/custom_settings.json" # The custom_settings.json contains a
@@ -536,9 +536,10 @@ Using ````parse_remote```` IdP metadata can be obtained and added to the setting
idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote('https://example.com/auth/saml2/idp/metadata')
``
-If the Metadata contains several entities, the relevant EntityDescriptor can be specified when retrieving the settings from the IdpMetadataParser by its Entity Id value:
-
+If the Metadata contains several entities, the relevant ``EntityDescriptor`` can be specified when retrieving the settings from the ``IdpMetadataParser`` by its ``EntityId`` value:
+```
idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote(https://example.com/metadatas, entity_id='idp_entity_id')
+```
#### How load the library ####
@@ -554,7 +555,7 @@ from onelogin.saml2.utils import OneLogin_Saml2_Utils
#### The Request ####
-Building an OneLogin_Saml2_Auth object requires a 'request' parameter.
+Building an ``OneLogin_Saml2_Auth object`` requires a ``request`` parameter.
```python
auth = OneLogin_Saml2_Auth(req)
@@ -573,7 +574,7 @@ req = {
}
```
-Each python framework built its own request object, you may map its data to match what the saml toolkit expects.
+Each Python framework built its own ``request`` object, you may map its data to match what the SAML toolkit expects.
Let`s see some examples:
```python
@@ -596,7 +597,7 @@ def prepare_from_flask_request(request):
'post_data': request.form.copy()
}
```
-The https dictionary entry should be set to on for https requests and off for http
+The ``https`` dictionary entry should be set to ``on`` for https requests and ``off`` for http
#### Initiate SSO ####
@@ -612,21 +613,22 @@ auth = OneLogin_Saml2_Auth(req) # Constructor of the SP, loads settings.json
auth.login() # Method that builds and sends the AuthNRequest
```
-The AuthNRequest will be sent signed or unsigned based on the security info of the advanced_settings.json ('authnRequestsSigned').
+The ``AuthNRequest`` will be sent signed or unsigned based on the security info of the ``advanced_settings.json`` (``authnRequestsSigned``).
-The IdP will then return the SAML Response to the user's client. The client is then forwarded to the Attribute Consumer Service of the SP with this information.
+The IdP will then return the SAML Response to the user's client. The client is then forwarded to the **Attribute Consumer Service (ACS)** of the SP with this information.
-We can set a 'return_to' url parameter to the login function and that will be converted as a 'RelayState' parameter:
+We can set a ``return_to`` url parameter to the login function and that will be converted as a ``RelayState`` parameter:
```python
target_url = 'https://example.com'
auth.login(return_to=target_url)
```
-The login method can recieve 3 more optional parameters:
+The login method can recieve 4 more optional parameters:
-* force_authn When true the AuthNReuqest will set the ForceAuthn='true'
-* is_passive When true the AuthNReuqest will set the Ispassive='true'
-* set_nameid_policy When true the AuthNReuqest will set a nameIdPolicy element.
+* ``force_authn`` When ``true`` the ``AuthNReuqest`` will set the ``ForceAuthn='true'``
+* ``is_passive`` When ``true`` the ``AuthNReuqest`` will set the ``Ispassive='true'``
+* ``set_nameid_policy`` When ``true`` the ``AuthNReuqest`` will set a ``nameIdPolicy`` element.
+* ``name_id_value_req`` Indicates to the IdP the ``Subject`` that should be authenticated
If a match on the future SAMLResponse ID and the AuthNRequest ID to be sent is required, that AuthNRequest ID must to be extracted and stored for future validation, we can get that ID by
@@ -655,17 +657,17 @@ else:
print "Error found on Metadata: %s" % (', '.join(errors))
```
-The get_sp_metadata will return the metadata signed or not based on the security info of the advanced_settings.json ('signMetadata').
+The ``get_sp_metadata`` will return the metadata signed or not based on the security info of the ``advanced_settings.json`` (``signMetadata``).
Before the XML metadata is exposed, a check takes place to ensure that the info to be provided is valid.
-Instead of using the Auth object, you can directly use
+Instead of using the ``Auth`` object, you can directly use
```
saml_settings = OneLogin_Saml2_Settings(settings=None, custom_base_path=None, sp_validation_only=True)
```
-to get the settings object and with the sp_validation_only=True parameter we will avoid the IdP Settings validation.
+to get the settings object and with the ``sp_validation_only=True`` parameter we will avoid the IdP settings validation.
-*** Attribute Consumer Service(ACS) ***
+*** Attribute Consumer Service (ACS) ***
This code handles the SAML response that the IdP forwards to the SP through the user's client.
@@ -693,10 +695,10 @@ The SAML response is processed and then checked that there are no errors. It als
At that point there are 2 possible alternatives:
-* If no RelayState is provided, we could show the user data in this view or however we wanted.
-* If RelayState is provided, a rediretion take place.
+* If no ``RelayState`` is provided, we could show the user data in this view or however we wanted.
+* If ``RelayState`` is provided, a rediretion take place.
-Notice that we saved the user data in the session before the redirection to have the user data available at the RelayState view.
+Notice that we saved the user data in the session before the redirection to have the user data available at the ``RelayState`` view.
In order to retrieve attributes we use:
@@ -728,7 +730,7 @@ print attributes['cn']
print auth.get_attribute('cn')
```
-Before trying to get an attribute, check that the user is authenticated. If the user isn't authenticated, an empty dict will be returned. For example, if we call to get_attributes before a auth.process_response, the get_attributes() will return an empty dict.
+Before trying to get an attribute, check that the user is authenticated. If the user isn't authenticated, an empty dict will be returned. For example, if we call to ``auth.get_attributes`` before a ``auth.process_response``, the ``auth.get_attributes`` will return an empty dict.
*** Single Logout Service (SLS) ***
@@ -789,7 +791,7 @@ else:
return self.redirect_to(self.get_slo_url(), parameters)
```
-If we don't want that process_slo to destroy the session, pass a true parameter to the process_slo method
+If we don't want that ``process_slo`` to destroy the session, pass a ``true`` parameter to the ``process_slo`` method
```python
keepLocalSession = true
@@ -809,11 +811,11 @@ auth = OneLogin_Saml2_Auth(req) # Constructor of the SP, loads settings.json
auth.logout() # Method that builds and sends the LogoutRequest
```
-The Logout Request will be sent signed or unsigned based on the security info of the advanced_settings.json ('logoutRequestSigned').
+The Logout Request will be sent signed or unsigned based on the security info of the ``advanced_settings.json`` (``logoutRequestSigned``).
-The IdP will return the Logout Response through the user's client to the Single Logout Service of the SP.
+The IdP will return the Logout Response through the user's client to the Single Logout Service (SLS) of the SP.
-We can set a 'return_to' url parameter to the logout function and that will be converted as a 'RelayState' parameter:
+We can set a ``return_to`` url parameter to the logout function and that will be converted as a ``RelayState`` parameter:
```python
target_url = 'https://example.com'
@@ -822,17 +824,17 @@ auth.logout(return_to=target_url)
Also there are 4 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
+* ``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
+* ``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
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
+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:
```python
auth.get_last_request_id()
@@ -890,7 +892,7 @@ else:
### SP Key rollover ###
-If you plan to update the SP x509cert and privateKey you can define the new x509cert as settings['sp']['x509certNew'] and it will be
+If you plan to update the SP X.509 cert and privateKey you can define the new X.509 cert as ``settings['sp']['x509certNew']`` and it will be
published on the SP metadata so Identity Providers can read them and get ready for rollover.
@@ -899,20 +901,20 @@ 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']['x509certMulti'] parameter.
+In order to handle that the toolkit offers the ``settings['idp']['x509certMulti']`` parameter.
-When that parameter is used, 'x509cert' 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 '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
+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
### Replay attacks ###
In order to avoid reply attacks, you can store the ID of the SAML messages already processed, to avoid processing them twice. Since the Messages expires and will be invalidated due that fact, you don't need to store those IDs longer than the time frame that you currently accepting.
- Get the ID of the last processed message/assertion with the get_last_message_id/get_last_assertion_id method of the Auth object.
+ Get the ID of the last processed message/assertion with the ``get_last_message_id``/``get_last_assertion_id method`` of the ``Auth`` object.
### Main classes and methods ###
@@ -932,30 +934,30 @@ Main class of OneLogin Python Toolkit
* ***is_authenticated*** Checks if the user is authenticated or not.
* ***get_attributes*** Returns the set of SAML attributes.
* ***get_attribute*** Returns the requested SAML attribute.
-* ***get_nameid*** Returns the nameID.
-* ***get_session_index*** Gets the SessionIndex from the AuthnStatement.
-* ***get_session_expiration*** Gets the SessionNotOnOrAfter from the AuthnStatement.
+* ***get_nameid*** Returns the ``nameID``.
+* ***get_session_index*** Gets the ``SessionIndex`` from the ``AuthnStatement``.
+* ***get_session_expiration*** Gets the ``SessionNotOnOrAfter`` from the ``AuthnStatement``.
* ***get_errors*** Returns a list with code errors if something went wrong.
* ***get_last_error_reason*** Returns the reason of the last error
* ***get_sso_url*** Gets the SSO url.
* ***get_slo_url*** Gets the SLO url.
-* ***get_last_request_id*** The ID of the last Request SAML message generated (AuthNRequest, LogoutRequest).
+* ***get_last_request_id*** The ``ID`` of the last Request SAML message generated (``AuthNRequest``, ``LogoutRequest``).
* ***get_last_authn_contexts*** Returns the list of authentication contexts sent in the last SAML Response.
* ***build_request_signature*** Builds the Signature of the SAML Request.
* ***build_response_signature*** Builds the Signature of the SAML Response.
* ***get_settings*** Returns the settings info.
* ***set_strict*** Set the strict mode active/disable.
* ***get_last_request_xml*** Returns the most recently-constructed/processed XML SAML request (AuthNRequest, LogoutRequest)
-* ***get_last_response_xml*** Returns the most recently-constructed/processed XML SAML response (SAMLResponse, LogoutResponse). If the SAMLResponse had an encrypted assertion, decrypts it.
-* ***get_last_message_id*** The ID of the last Response SAML message processed.
-* ***get_last_assertion_id*** The ID of the last assertion processed.
-* ***get_last_assertion_not_on_or_after*** The NotOnOrAfter value of the valid SubjectConfirmationData node (if any) of the last assertion processed (is only calculated with strict = true)
+* ***get_last_response_xml*** Returns the most recently-constructed/processed XML SAML response (``SAMLResponse``, ``LogoutResponse``). If the SAMLResponse had an encrypted assertion, decrypts it.
+* ***get_last_message_id*** The ``ID`` of the last Response SAML message processed.
+* ***get_last_assertion_id*** The ``ID`` of the last assertion processed.
+* ***get_last_assertion_not_on_or_after*** The ``NotOnOrAfter`` value of the valid SubjectConfirmationData node (if any) of the last assertion processed (is only calculated with strict = true)
#### OneLogin_Saml2_Auth - authn_request.py ####
SAML 2 Authentication Request class
-* `__init__` This class handles an AuthNRequest. It builds an AuthNRequest object.
+* `__init__` This class handles an AuthNRequest. It builds an ``AuthNRequest`` object.
* ***get_request*** Returns unsigned AuthnRequest.
* ***get_id*** Returns the AuthNRequest ID.
* ***get_xml*** Returns the XML that will be sent as part of the request.
@@ -969,30 +971,30 @@ SAML 2 Authentication Response class
* ***check_status*** Check if the status of the response is success or not
* ***get_audiences*** Gets the audiences
* ***get_issuers*** Gets the issuers (from message and from assertion)
-* ***get_nameid_data*** Gets the NameID Data provided by the SAML Response from the IdP (returns a dict)
+* ***get_nameid_data*** Gets the ``NameID`` Data provided by the SAML Response from the IdP (returns a dict)
* ***get_nameid*** Gets the NameID provided by the SAML Response from the IdP (returns a string)
* ***get_session_not_on_or_after*** Gets the SessionNotOnOrAfter from the AuthnStatement
-* ***get_session_index*** Gets the SessionIndex from the AuthnStatement
-* ***get_attributes*** Gets the Attributes from the AttributeStatement element.
+* ***get_session_index*** Gets the ``SessionIndex`` from the ``AuthnStatement``
+* ***get_attributes*** Gets the ``Attributes`` from the ``AttributeStatement`` element.
* ***validate_num_assertions*** Verifies that the document only contains a single Assertion (encrypted or not)
-* ***validate_timestamps*** Verifies that the document is valid according to Conditions Element
+* ***validate_timestamps*** Verifies that the document is valid according to ``Conditions`` element
* ***get_error*** After execute a validation process, if fails this method returns the cause
* ***get_xml_document*** Returns the SAML Response document (If contains an encrypted assertion, decrypts it).
* ***get_id*** the ID of the response
-* ***get_assertion_id*** the ID of the assertion in the response
-* ***get_assertion_not_on_or_after*** the NotOnOrAfter value of the valid SubjectConfirmationData if any
+* ***get_assertion_id*** the ``ID`` of the assertion in the response
+* ***get_assertion_not_on_or_after*** the ``NotOnOrAfter`` value of the valid SubjectConfirmationData if any
#### OneLogin_Saml2_LogoutRequest - logout_request.py ####
SAML 2 Logout Request class
* `__init__` Constructs the Logout Request object.
-* ***get_request*** Returns the Logout Request defated, base64encoded.
+* ***get_request*** Returns the Logout Request defated, base64-encoded.
* ***get_id*** Returns the ID of the Logout Request. (If you have the object you can access to the id attribute)
* ***get_nameid_data*** Gets the NameID Data of the the Logout Request (returns a dict).
* ***get_nameid*** Gets the NameID of the Logout Request Message (returns a string).
* ***get_issuer*** Gets the Issuer of the Logout Request Message.
-* ***get_session_indexes*** Gets the SessionIndexes from the Logout Request.
+* ***get_session_indexes*** Gets the ``SessionIndexes`` from the Logout Request.
* ***is_valid*** Checks if the Logout Request recieved is valid.
* ***get_error*** After execute a validation process, if fails this method returns the cause.
* ***get_xml*** Returns the XML that will be sent as part of the request or that was received at the SP
@@ -1027,11 +1029,11 @@ Configuration of the OneLogin Python Toolkit
* ***get_lib_path*** Returns lib path.
* ***get_ext_lib_path*** Returns external lib path.
* ***get_schemas_path*** Returns schema path.
-* ***check_sp_certs*** Checks if the x509 certs of the SP exists and are valid.
-* ***get_sp_key*** Returns the x509 private key of the SP.
-* ***get_sp_cert*** Returns the x509 public cert of the SP.
-* ***get_sp_cert_new*** Returns the future x509 public cert of the SP.
-* ***get_idp_cert*** Returns the x509 public cert of the IdP.
+* ***check_sp_certs*** Checks if the X.509 certs of the SP exists and are valid.
+* ***get_sp_key*** Returns the X.509 private key of the SP.
+* ***get_sp_cert*** Returns the X.509 public cert of the SP.
+* ***get_sp_cert_new*** Returns the future X.509 public cert of the SP.
+* ***get_idp_cert*** Returns the X.509 public cert of the IdP.
* ***get_sp_data*** Gets the SP data.
* ***get_idp_data*** Gets the IdP data.
* ***get_security_data*** Gets security data.
@@ -1043,7 +1045,7 @@ Configuration of the OneLogin Python Toolkit
* ***format_sp_cert_new*** Formats the SP cert new.
* ***format_sp_key*** Formats the private key.
* ***set_strict*** Activates or deactivates the strict mode.
-* ***is_strict*** Returns if the 'strict' mode is active.
+* ***is_strict*** Returns if the ``strict`` mode is active.
* ***is_debug_active*** Returns if the debug is active.
#### OneLogin_Saml2_Metadata - metadata.py ####
@@ -1052,7 +1054,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_x509_key_descriptors*** Adds the x509 descriptors (sign/encriptation) to the metadata
+* ***add_x509_key_descriptors*** Adds the X.509 descriptors (sign/encriptation) to the metadata
#### OneLogin_Saml2_Utils - utils.py ####
@@ -1061,7 +1063,7 @@ Auxiliary class that contains several methods
* ***decode_base64_and_inflate*** Base64 decodes and then inflates according to RFC1951.
* ***deflate_and_base64_encode*** Deflates and the base64 encodes a string.
* ***validate_xml*** Validates a xml against a schema.
-* ***format_cert*** Returns a x509 cert (adding header & footer if required).
+* ***format_cert*** Returns a X.509 cert (adding header & footer if required).
* ***format_private_key*** Returns a private key (adding header & footer if required).
* ***redirect*** Executes a redirection to the provided url (or return the target url).
* ***get_self_url_host*** Returns the protocol + the current host + the port (if different than common ports).
@@ -1078,7 +1080,7 @@ Auxiliary class that contains several methods
* ***get_expire_time*** Compares 2 dates and returns the earliest.
* ***query*** Extracts nodes that match the query from the Element.
* ***delete_local_session*** Deletes the local session.
-* ***calculate_x509_fingerprint*** Calculates the fingerprint of a x509cert.
+* ***calculate_x509_fingerprint*** Calculates the fingerprint of a X.509 cert.
* ***format_finger_print*** Formates a fingerprint.
* ***generate_name_id*** Generates a nameID.
* ***get_status*** Gets Status from a Response.
@@ -1099,7 +1101,7 @@ A class that contains methods to obtain and parse metadata from IdP
* ***parse*** Parse the Identity Provider metadata and returns a dict with extracted data
* ***merge_settings*** Will update the settings with the provided new settings data extracted from the IdP metadata
-For more info, look at the source code; each method is documented and details about what does and how to use it are provided. Make sure to also check the doc folder where HTML documentation about the classes and methods is provided.
+For more info, look at the source code. Each method is documented and details about what does and how to use it are provided. Make sure to also check the doc folder where HTML documentation about the classes and methods is provided.
@@ -1107,7 +1109,7 @@ For more info, look at the source code; each method is documented and details ab
Demos included in the toolkit
-----------------------------
-The toolkit includes 2 demos to teach how use the toolkit (A django and a flask project), take a look on it.
+The toolkit includes 4 demos to teach how use the toolkit (Django, Flask, Pyramid and Bootle projects), take a look on them.
Demos require that SP and IdP are well configured before test it, so edit the settings files.
Notice that each python framework has it own way to handle routes/urls and process request, so focus on
@@ -1115,8 +1117,7 @@ how it deployed. New demos using other python frameworks are welcome as a contri
### Getting Started ###
-We said that this toolkit includes a django application demo and a flask applicacion demo,
-lets see how fast is deploy them.
+We said that this toolkit includes a demos, lets see how fast is deploy some of them.
*** Virtualenv ***
@@ -1161,7 +1162,7 @@ Now, with the virtualenv loaded, you can run the demo like this:
python index.py
```
-You'll have the demo running at http://localhost:8000
+You'll have the demo running at ``http://localhost:8000``
#### Content ####
@@ -1170,42 +1171,42 @@ The flask project contains:
* ***index.py*** Is the main flask file, where or the SAML handle take place.
-* ***templates***. Is the folder where flask stores the templates of the project. It was implemented a base.html template that is extended by index.html and attrs.html, the templates of our simple demo that shows messages, user attributes when available and login and logout links.
+* ***templates***. Is the folder where flask stores the templates of the project. It was implemented a base.html template that is extended by ``index.html`` and ``attrs.html``, the templates of our simple demo that shows messages, user attributes when available and login and logout links.
-* ***saml*** Is a folder that contains the 'certs' folder that could be used to store the x509 public and private key, and the saml toolkit settings (settings.json and advanced_settings.json).
+* ***saml*** Is a folder that contains the 'certs' folder that could be used to store the X.509 public and private key, and the saml toolkit settings (``settings.json`` and ``advanced_settings.json``).
#### SP setup ####
-The Onelogin's Python Toolkit allows you to provide the settings info in 2 ways: settings files or define a setting dict. In the demo-flask it used the first method.
+The Onelogin's Python Toolkit allows you to provide the settings info in 2 ways: Settings files or define a setting dict. In the ``demo-flask``, it uses the first method.
-In the index.py file we define the app.config['SAML_PATH'], that will target to the 'saml' folder. We require it in order to load the settings files.
+In the index.py file we define the ``app.config['SAML_PATH']``, that will target to the ``saml`` folder. We require it in order to load the settings files.
-First we need to edit the saml/settings.json, configure the SP part and review the metadata of the IdP and complete the IdP info. Later edit the saml/advanced_settings.json files and configure the how the toolkit will work. Check the settings section of this document if you have any doubt.
+First we need to edit the ``saml/settings.json``, configure the SP part and review the metadata of the IdP and complete the IdP info. Later edit the saml/advanced_settings.json files and configure the how the toolkit will work. Check the settings section of this document if you have any doubt.
#### IdP setup ####
-Once the SP is configured, the metadata of the SP is published at the /metadata url. Based on that info, configure the IdP.
+Once the SP is configured, the metadata of the SP is published at the ``/metadata`` url. Based on that info, configure the IdP.
#### How it works ####
-1. First time you access to the main view 'http://localhost:8000', you can select to login and return to the same view or login and be redirected to /?attrs (attrs view).
+1. First time you access to the main view ``http://localhost:8000``, you can select to login and return to the same view or login and be redirected to ``/?attrs`` (attrs view).
2. When you click:
- 2.1 in the first link, we access to /?sso (index view). An AuthNRequest is sent to the IdP, we authenticate at the IdP and then a Response is sent through the user's client to the SP, specifically the Assertion Consumer Service view: /?acs. Notice that a RelayState parameter is set to the url that initiated the process, the index view.
+ 2.1 in the first link, we access to ``/?sso`` (index view). An ``AuthNRequest`` is sent to the IdP, we authenticate at the IdP and then a Response is sent through the user's client to the SP, specifically the Assertion Consumer Service view: /?acs. Notice that a RelayState parameter is set to the url that initiated the process, the index view.
- 2.2 in the second link we access to /?attrs (attrs view), we will expetience have the same process described at 2.1 with the diference that as RelayState is set the attrs url.
+ 2.2 in the second link we access to ``/?attrs`` (attrs view), we will expetience have the same process described at 2.1 with the diference that as ``RelayState`` is set the attrs url.
- 3. The SAML Response is processed in the ACS /?acs, if the Response is not valid, the process stops here and a message is shown. Otherwise we are redirected to the RelayState view. a) / or b) /?attrs
+ 3. The ``SAMLResponse`` is processed in the ACS ``/?acs``, if the Response is not valid, the process stops here and a message is shown. Otherwise we are redirected to the ``RelayState`` view. a) / or b) ``/?attrs``
4. We are logged in the app and the user attributes are showed. At this point, we can test the single log out functionality.
The single log out funcionality could be tested by 2 ways.
- 5.1 SLO Initiated by SP. Click on the "logout" link at the SP, after that a Logout Request is sent to the IdP, the session at the IdP is closed and replies through the client to the SP with a Logout Response (sent to the Single Logout Service endpoint). The SLS endpoint /?sls of the SP process the Logout Response and if is valid, close the user session of the local app. Notice that the SLO Workflow starts and ends at the SP.
+ 5.1 SLO Initiated by SP. Click on the ``logout`` link at the SP, after that a Logout Request is sent to the IdP, the session at the IdP is closed and replies through the client to the SP with a Logout Response (sent to the Single Logout Service endpoint). The SLS endpoint ``/?sls`` of the SP process the Logout Response and if is valid, close the user session of the local app. Notice that the SLO Workflow starts and ends at the SP.
- 5.2 SLO Initiated by IdP. In this case, the action takes place on the IdP side, the logout process is initiated at the IdP, sends a Logout Request to the SP (SLS endpoint, /?sls). The SLS endpoint of the SP process the Logout Request and if is valid, close the session of the user at the local app and send a Logout Response to the IdP (to the SLS endpoint of the IdP). The IdP receives the Logout Response, process it and close the session at of the IdP. Notice that the SLO Workflow starts and ends at the IdP.
+ 5.2 SLO Initiated by IdP. In this case, the action takes place on the IdP side, the logout process is initiated at the IdP, sends a Logout Request to the SP (SLS endpoint, ``/?sls``). The SLS endpoint of the SP process the Logout Request and if is valid, close the session of the user at the local app and send a Logout Response to the IdP (to the SLS endpoint of the IdP). The IdP receives the Logout Response, process it and close the session at of the IdP. Notice that the SLO Workflow starts and ends at the IdP.
Notice that all the SAML Requests and Responses are handled at a unique view (index) and how GET paramters are used to know the action that must be done.
@@ -1226,7 +1227,7 @@ Later, with the virtualenv loaded, you can run the demo like this:
python manage.py runserver 0.0.0.0:8000
```
-You'll have the demo running at http://localhost:8000.
+You'll have the demo running at ``http://localhost:8000``.
Note that many of the configuration files expect HTTPS. This is not required by the demo, as replacing these SP URLs with HTTP will work just fine. HTTPS is however highly encouraged, and left as an exercise for the reader for their specific needs.
@@ -1236,33 +1237,33 @@ If you want to integrate a production django application, take a look on this SA
The django project contains:
-* ***manage.py***. A file that is automatically created in each Django project. Is a thin wrapper around django-admin.py that takes care of putting the project’s package on sys.path and sets the DJANGO_SETTINGS_MODULE environment variable.
+* ***manage.py***. A file that is automatically created in each Django project. Is a thin wrapper around ``django-admin.py`` that takes care of putting the project’s package on ``sys.path`` and sets the ``DJANGO_SETTINGS_MODULE`` environment variable.
-* ***saml*** Is a folder that contains the 'certs' folder that could be used to store the x509 public and private key, and the saml toolkit settings (settings.json and advanced_settings.json).
+* ***saml*** Is a folder that contains the ``certs`` folder that could be used to store the X.509 public and private key, and the saml toolkit settings (``settings.json`` and ``advanced_settings.json``).
* ***demo*** Is the main folder of the django project, that contains the typical files:
* ***settings.py*** Contains the default parameters of a django project except the SAML_FOLDER parameter, that may contain the path where is located the 'saml' folder.
- * ***urls.py*** A file that define url routes. In the demo we defined '/' that is related to the index view, '/attrs' that is related with the attrs view and '/metadata', related to th metadata view.
+ * ***urls.py*** A file that define url routes. In the demo we defined ``/`` that is related to the index view, ``/attrs`` that is related with the attrs view and ``/metadata``, related to th metadata view.
* ***views.py*** This file contains the views of the django project and some aux methods.
* ***wsgi.py*** A file that let as deploy django using WSGI, the Python standard for web servers and applications.
-* ***templates***. Is the folder where django stores the templates of the project. It was implemented a base.html template that is extended by index.html and attrs.html, the templates of our simple demo that shows messages, user attributes when available and login and logout links.
+* ***templates***. Is the folder where django stores the templates of the project. It was implemented a base.html template that is extended by ``index.html`` and ``attrs.html``, the templates of our simple demo that shows messages, user attributes when available and login and logout links.
#### SP setup ####
-The Onelogin's Python Toolkit allows you to provide the settings info in 2 ways: settings files or define a setting dict. In the demo-django it used the first method.
+The Onelogin's Python Toolkit allows you to provide the settings info in 2 ways: Settings files or define a setting dict. In the ``demo-django``, it uses the first method.
-After set the SAML_FOLDER in the demo/settings.py, the settings of the python toolkit will be loaded on the django web.
+After set the ``SAML_FOLDER`` in the ``demo/settings.py``, the settings of the python toolkit will be loaded on the django web.
-First we need to edit the saml/settings.json, configure the SP part and review the metadata of the IdP and complete the IdP info. Later edit the saml/advanced_settings.json files and configure the how the toolkit will work. Check the settings section of this document if you have any doubt.
+First we need to edit the ``saml/settings.json``, configure the SP part and review the metadata of the IdP and complete the IdP info. Later edit the saml/advanced_settings.json files and configure the how the toolkit will work. Check the settings section of this document if you have any doubt.
#### IdP setup ####
-Once the SP is configured, the metadata of the SP is published at the /metadata url. Based on that info, configure the IdP.
+Once the SP is configured, the metadata of the SP is published at the ``/metadata`` url. Based on that info, configure the IdP.
#### How it works ####
-This demo works very similar to the flask-demo (We did it intentionally).
+This demo works very similar to the ``flask-demo`` (We did it intentionally).
### Demo Pyramid ###
@@ -1279,7 +1280,7 @@ To run the demo you need to install Pyramid, the requirements, etc.:
env/bin/pip install -e ".[testing]"
```
-Next, edit the settings in `demo_pyramid/saml/settings.json`. (Pyramid runs on
+Next, edit the settings in ``demo_pyramid/saml/settings.json``. (Pyramid runs on
port 6543 by default.)
Now you can run the demo like this:
@@ -1287,7 +1288,7 @@ Now you can run the demo like this:
env/bin/pserve development.ini
```
-If that worked, the demo is now running at http://localhost:6543.
+If that worked, the demo is now running at ``http://localhost:6543``.
#### Content ####
@@ -1298,41 +1299,41 @@ The Pyramid project contains:
* ***views.py*** is where all the SAML handling takes place.
-* ***templates*** is the folder where Pyramid stores the templates of the project. It was implemented a layout.jinja2 template that is extended by index.jinja2 and attrs.jinja2, the templates of our simple demo that shows messages, user attributes when available and login and logout links.
+* ***templates*** is the folder where Pyramid stores the templates of the project. It was implemented a ``layout.jinja2`` template that is extended by ``index.jinja2`` and ``attrs.jinja2``, the templates of our simple demo that shows messages, user attributes when available and login and logout links.
-* ***saml*** is a folder that contains the 'certs' folder that could be used to store the x509 public and private key, and the saml toolkit settings (settings.json and advanced_settings.json).
+* ***saml*** is a folder that contains the 'certs' folder that could be used to store the X.509 public and private key, and the saml toolkit settings (``settings.json`` and ``advanced_settings.json``).
#### SP setup ####
-The Onelogin's Python Toolkit allows you to provide the settings info in 2 ways: settings files or define a setting dict. In demo_pyramid the first method is used.
+The Onelogin's Python Toolkit allows you to provide the settings info in 2 ways: Settings files or define a setting dict. In ``demo_pyramid`` the first method is used.
-In the views.py file we define the SAML_PATH, which will target the 'saml' folder. We require it in order to load the settings files.
+In the views.py file we define the ``SAML_PATH``, which will target the ``saml`` folder. We require it in order to load the settings files.
-First we need to edit the saml/settings.json, configure the SP part and review the metadata of the IdP and complete the IdP info. Later edit the saml/advanced_settings.json files and configure the how the toolkit will work. Check the settings section of this document if you have any doubt.
+First we need to edit the ``saml/settings.json``, configure the SP part and review the metadata of the IdP and complete the IdP info. Later edit the ``saml/advanced_settings.json`` files and configure the how the toolkit will work. Check the settings section of this document if you have any doubt.
#### IdP setup ####
-Once the SP is configured, the metadata of the SP is published at the /metadata/ url. Based on that info, configure the IdP.
+Once the SP is configured, the metadata of the SP is published at the ``/metadata/`` url. Based on that info, configure the IdP.
#### How it works ####
-1. First time you access to the main view 'http://localhost:6543', you can select to login and return to the same view or login and be redirected to /?attrs (attrs view).
+1. First time you access to the main view ``http://localhost:6543``, you can select to login and return to the same view or login and be redirected to ``/?attrs`` (attrs view).
2. When you click:
- 2.1 in the first link, we access to /?sso (index view). An AuthNRequest is sent to the IdP, we authenticate at the IdP and then a Response is sent through the user's client to the SP, specifically the Assertion Consumer Service view: /?acs. Notice that a RelayState parameter is set to the url that initiated the process, the index view.
+ 2.1 in the first link, we access to ``/?sso`` (index view). An ``AuthNRequest`` is sent to the IdP, we authenticate at the IdP and then a Response is sent through the user's client to the SP, specifically the Assertion Consumer Service view: ``/?acs``. Notice that a RelayState parameter is set to the url that initiated the process, the index view.
- 2.2 in the second link we access to /?attrs (attrs view), we will expetience have the same process described at 2.1 with the diference that as RelayState is set the attrs url.
+ 2.2 in the second link we access to ``/?attrs`` (attrs view), we will expetience have the same process described at 2.1 with the diference that as ``RelayState`` is set the attrs url.
- 3. The SAML Response is processed in the ACS /?acs, if the Response is not valid, the process stops here and a message is shown. Otherwise we are redirected to the RelayState view. a) / or b) /?attrs
+ 3. The SAML Response is processed in the ACS ``/?acs``, if the Response is not valid, the process stops here and a message is shown. Otherwise we are redirected to the ``RelayState`` view. a) / or b) ``/?attrs``
4. We are logged in the app and the user attributes are showed. At this point, we can test the single log out functionality.
The single log out funcionality could be tested by 2 ways.
- 5.1 SLO Initiated by SP. Click on the "logout" link at the SP, after that a Logout Request is sent to the IdP, the session at the IdP is closed and replies through the client to the SP with a Logout Response (sent to the Single Logout Service endpoint). The SLS endpoint /?sls of the SP process the Logout Response and if is valid, close the user session of the local app. Notice that the SLO Workflow starts and ends at the SP.
+ 5.1 SLO Initiated by SP. Click on the ``logout`` link at the SP, after that a Logout Request is sent to the IdP, the session at the IdP is closed and replies through the client to the SP with a Logout Response (sent to the Single Logout Service endpoint). The SLS endpoint ``/?sls`` of the SP process the Logout Response and if is valid, close the user session of the local app. Notice that the SLO Workflow starts and ends at the SP.
- 5.2 SLO Initiated by IdP. In this case, the action takes place on the IdP side, the logout process is initiated at the IdP, sends a Logout Request to the SP (SLS endpoint, /?sls). The SLS endpoint of the SP process the Logout Request and if is valid, close the session of the user at the local app and send a Logout Response to the IdP (to the SLS endpoint of the IdP). The IdP receives the Logout Response, process it and close the session at of the IdP. Notice that the SLO Workflow starts and ends at the IdP.
+ 5.2 SLO Initiated by IdP. In this case, the action takes place on the IdP side, the logout process is initiated at the IdP, sends a Logout Request to the SP (SLS endpoint, ``/?sls``). The SLS endpoint of the SP process the Logout Request and if is valid, close the session of the user at the local app and send a Logout Response to the IdP (to the SLS endpoint of the IdP). The IdP receives the Logout Response, process it and close the session at of the IdP. Notice that the SLO Workflow starts and ends at the IdP.
Notice that all the SAML Requests and Responses are handled at a unique view (index) and how GET parameters are used to know the action that must be done.
diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py
index 722c1c22..d9b42a32 100644
--- a/src/onelogin/saml2/auth.py
+++ b/src/onelogin/saml2/auth.py
@@ -323,7 +323,7 @@ def get_last_assertion_id(self):
"""
return self.__last_assertion_id
- 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.
@@ -339,10 +339,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()
saml_request = authn_request.get_request()
diff --git a/src/onelogin/saml2/authn_request.py b/src/onelogin/saml2/authn_request.py
index 009c7233..d7be43fe 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
@@ -69,6 +72,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']
@@ -110,7 +121,7 @@ def __init__(self, settings, force_authn=False, is_passive=False, set_nameid_pol
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%(requested_authn_context_str)s
+ %(entity_id)s %(subject_str)s%(nameid_policy_str)s%(requested_authn_context_str)s
""" % \
{
'id': uid,
@@ -121,6 +132,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/tests/data/metadata/testshib-providers.xml b/tests/data/metadata/testshib-providers.xml
index c00a1b77..47c2a873 100644
--- a/tests/data/metadata/testshib-providers.xml
+++ b/tests/data/metadata/testshib-providers.xml
@@ -37,28 +37,23 @@
- MIIEDjCCAvagAwIBAgIBADANBgkqhkiG9w0BAQUFADBnMQswCQYDVQQGEwJVUzEV
- MBMGA1UECBMMUGVubnN5bHZhbmlhMRMwEQYDVQQHEwpQaXR0c2J1cmdoMREwDwYD
- VQQKEwhUZXN0U2hpYjEZMBcGA1UEAxMQaWRwLnRlc3RzaGliLm9yZzAeFw0wNjA4
- MzAyMTEyMjVaFw0xNjA4MjcyMTEyMjVaMGcxCzAJBgNVBAYTAlVTMRUwEwYDVQQI
- EwxQZW5uc3lsdmFuaWExEzARBgNVBAcTClBpdHRzYnVyZ2gxETAPBgNVBAoTCFRl
- c3RTaGliMRkwFwYDVQQDExBpZHAudGVzdHNoaWIub3JnMIIBIjANBgkqhkiG9w0B
- AQEFAAOCAQ8AMIIBCgKCAQEArYkCGuTmJp9eAOSGHwRJo1SNatB5ZOKqDM9ysg7C
- yVTDClcpu93gSP10nH4gkCZOlnESNgttg0r+MqL8tfJC6ybddEFB3YBo8PZajKSe
- 3OQ01Ow3yT4I+Wdg1tsTpSge9gEz7SrC07EkYmHuPtd71CHiUaCWDv+xVfUQX0aT
- NPFmDixzUjoYzbGDrtAyCqA8f9CN2txIfJnpHE6q6CmKcoLADS4UrNPlhHSzd614
- kR/JYiks0K4kbRqCQF0Dv0P5Di+rEfefC6glV8ysC8dB5/9nb0yh/ojRuJGmgMWH
- gWk6h0ihjihqiu4jACovUZ7vVOCgSE5Ipn7OIwqd93zp2wIDAQABo4HEMIHBMB0G
- A1UdDgQWBBSsBQ869nh83KqZr5jArr4/7b+QazCBkQYDVR0jBIGJMIGGgBSsBQ86
- 9nh83KqZr5jArr4/7b+Qa6FrpGkwZzELMAkGA1UEBhMCVVMxFTATBgNVBAgTDFBl
- bm5zeWx2YW5pYTETMBEGA1UEBxMKUGl0dHNidXJnaDERMA8GA1UEChMIVGVzdFNo
- aWIxGTAXBgNVBAMTEGlkcC50ZXN0c2hpYi5vcmeCAQAwDAYDVR0TBAUwAwEB/zAN
- BgkqhkiG9w0BAQUFAAOCAQEAjR29PhrCbk8qLN5MFfSVk98t3CT9jHZoYxd8QMRL
- I4j7iYQxXiGJTT1FXs1nd4Rha9un+LqTfeMMYqISdDDI6tv8iNpkOAvZZUosVkUo
- 93pv1T0RPz35hcHHYq2yee59HJOco2bFlcsH8JBXRSRrJ3Q7Eut+z9uo80JdGNJ4
- /SJy5UorZ8KazGj16lfJhOBXldgrhppQBb0Nq6HKHguqmwRfJ+WkxemZXzhediAj
- Geka8nz8JjwxpUjAiSWYKLtJhGEaTqCYxCCX2Dw+dOTqUzHOZ7WKv4JXPK5G/Uhr
- 8K/qhmFT2nIQi538n6rVYLeWj8Bbnl+ev0peYzxFyF5sQA==
+ MIIDAzCCAeugAwIBAgIVAPX0G6LuoXnKS0Muei006mVSBXbvMA0GCSqGSIb3DQEB
+ CwUAMBsxGTAXBgNVBAMMEGlkcC50ZXN0c2hpYi5vcmcwHhcNMTYwODIzMjEyMDU0
+ WhcNMzYwODIzMjEyMDU0WjAbMRkwFwYDVQQDDBBpZHAudGVzdHNoaWIub3JnMIIB
+ IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAg9C4J2DiRTEhJAWzPt1S3ryh
+ m3M2P3hPpwJwvt2q948vdTUxhhvNMuc3M3S4WNh6JYBs53R+YmjqJAII4ShMGNEm
+ lGnSVfHorex7IxikpuDPKV3SNf28mCAZbQrX+hWA+ann/uifVzqXktOjs6DdzdBn
+ xoVhniXgC8WCJwKcx6JO/hHsH1rG/0DSDeZFpTTcZHj4S9MlLNUtt5JxRzV/MmmB
+ 3ObaX0CMqsSWUOQeE4nylSlp5RWHCnx70cs9kwz5WrflnbnzCeHU2sdbNotBEeTH
+ ot6a2cj/pXlRJIgPsrL/4VSicPZcGYMJMPoLTJ8mdy6mpR6nbCmP7dVbCIm/DQID
+ AQABoz4wPDAdBgNVHQ4EFgQUUfaDa2mPi24x09yWp1OFXmZ2GPswGwYDVR0RBBQw
+ EoIQaWRwLnRlc3RzaGliLm9yZzANBgkqhkiG9w0BAQsFAAOCAQEASKKgqTxhqBzR
+ OZ1eVy++si+eTTUQZU4+8UywSKLia2RattaAPMAcXUjO+3cYOQXLVASdlJtt+8QP
+ dRkfp8SiJemHPXC8BES83pogJPYEGJsKo19l4XFJHPnPy+Dsn3mlJyOfAa8RyWBS
+ 80u5lrvAcr2TJXt9fXgkYs7BOCigxtZoR8flceGRlAZ4p5FPPxQR6NDYb645jtOT
+ MVr3zgfjP6Wh2dt+2p04LG7ENJn8/gEwtXVuXCsPoSCDx9Y0QmyXTJNdV1aB0AhO
+ RkWPlFYwp+zOyOIR+3m1+pqWFpn0eT/HrxpdKa74FA3R2kq4R7dXe4G0kUgXTdqX
+ MLRKhDgdmA==
diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py
index 3c36d5a4..12e66465 100644
--- a/tests/src/OneLogin/saml2_tests/auth_test.py
+++ b/tests/src/OneLogin/saml2_tests/auth_test.py
@@ -615,7 +615,7 @@ def testLoginSigned(self):
def testLoginForceAuthN(self):
"""
Tests the login method of the OneLogin_Saml2_Auth class
- Case Login 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'
@@ -649,7 +649,7 @@ def testLoginForceAuthN(self):
def testLoginIsPassive(self):
"""
Tests the login method of the OneLogin_Saml2_Auth class
- Case Login 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'
@@ -683,7 +683,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'
@@ -714,6 +714,47 @@ def testLoginSetNameIDPolicy(self):
request_3 = 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 = 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 3dd3e9df..5190d84a 100644
--- a/tests/src/OneLogin/saml2_tests/authn_request_test.py
+++ b/tests/src/OneLogin/saml2_tests/authn_request_test.py
@@ -262,6 +262,38 @@ def testCreateRequestSetNameIDPolicy(self):
self.assertRegexpMatches(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()
+ decoded_2 = b64decode(authn_request_encoded_2)
+ inflated_2 = decompress(decoded_2, -15)
+ self.assertRegexpMatches(inflated_2, '^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()
+ decoded_3 = b64decode(authn_request_encoded_3)
+ inflated_3 = decompress(decoded_3, -15)
+ self.assertRegexpMatches(inflated_3, '^testuser@example.com', inflated_3)
+ self.assertIn('', inflated_3)
+
def testCreateDeflatedSAMLRequestURLParameter(self):
"""
Tests the OneLogin_Saml2_Authn_Request Constructor.
From d61af25f924759c4d3fd71413b7e563acbf7775d Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Tue, 2 Apr 2019 14:17:19 +0200
Subject: [PATCH 170/255] Fix pycodestyle
---
tests/src/OneLogin/saml2_tests/auth_test.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py
index 12e66465..070a4887 100644
--- a/tests/src/OneLogin/saml2_tests/auth_test.py
+++ b/tests/src/OneLogin/saml2_tests/auth_test.py
@@ -754,7 +754,6 @@ def testLoginWithSubject(self):
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
From 9fe7a72da5b4caa1529c1640b52d2649447ce49b Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Fri, 26 Apr 2019 11:36:45 +0200
Subject: [PATCH 171/255] Update defusedxml
---
setup.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/setup.py b/setup.py
index be6c5d69..74f81d58 100644
--- a/setup.py
+++ b/setup.py
@@ -34,7 +34,7 @@
install_requires=[
'dm.xmlsec.binding==1.3.7',
'isodate>=0.5.0',
- 'defusedxml>=0.4.1',
+ 'defusedxml>=0.6.0',
],
extras_require={
'test': (
From b6741fdabfb89377d63a93dc9e0c08efa8b71f85 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Mon, 24 Jun 2019 20:30:27 +0200
Subject: [PATCH 172/255] fix path in flask demo
---
demo-flask/index.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/demo-flask/index.py b/demo-flask/index.py
index ac2c1320..0f70b777 100644
--- a/demo-flask/index.py
+++ b/demo-flask/index.py
@@ -11,7 +11,7 @@
app = Flask(__name__)
app.config['SECRET_KEY'] = 'onelogindemopytoolkit'
-app.config['SAML_PATH'] = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'saml')
+app.config['SAML_PATH'] = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'saml')
def init_saml_auth(req):
From bd86f1e780fa4c9dfe37fce944951a0268ab373d Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Thu, 27 Jun 2019 16:33:33 +0200
Subject: [PATCH 173/255] 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
---
README.md | 16 ++++-
demo-django/demo/settings.py | 3 +-
demo-django/demo/views.py | 14 +++-
demo-django/saml/settings.json | 20 +++---
demo-flask/index.py | 38 +++++++++--
src/onelogin/saml2/auth.py | 30 +++++++-
src/onelogin/saml2/logout_request.py | 24 ++++---
src/onelogin/saml2/response.py | 26 +++++++
tests/src/OneLogin/saml2_tests/auth_test.py | 67 ++++++++++++++++++
.../saml2_tests/logout_request_test.py | 1 -
.../src/OneLogin/saml2_tests/response_test.py | 68 +++++++++++++++++++
11 files changed, 271 insertions(+), 36 deletions(-)
diff --git a/README.md b/README.md
index 3f70a5b1..5f6865a5 100644
--- a/README.md
+++ b/README.md
@@ -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:
@@ -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
@@ -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
diff --git a/demo-django/demo/settings.py b/demo-django/demo/settings.py
index 792ea82f..5305f382 100644
--- a/demo-django/demo/settings.py
+++ b/demo-django/demo/settings.py
@@ -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
diff --git a/demo-django/demo/views.py b/demo-django/demo/views.py
index 2ae0ebac..40ebdd66 100644
--- a/demo-django/demo/views.py
+++ b/demo-django/demo/views.py
@@ -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)
@@ -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']))
diff --git a/demo-django/saml/settings.json b/demo-django/saml/settings.json
index 391b91c1..3758746c 100644
--- a/demo-django/saml/settings.json
+++ b/demo-django/saml/settings.json
@@ -2,29 +2,29 @@
"strict": true,
"debug": true,
"sp": {
- "entityId": "https:///metadata/",
+ "entityId": "http://pitbulk.no-ip.org:8000/metadata/",
"assertionConsumerService": {
- "url": "https:///?acs",
+ "url": "http://pitbulk.no-ip.org:8000/?acs",
"binding": "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
},
"singleLogoutService": {
- "url": "https:///?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/",
+ "entityId": "https://app.onelogin.com/saml/metadata/3dbd155e-be64-4a4d-8fab-e44788bce74f",
"singleSignOnService": {
- "url": "https://app.onelogin.com/trust/saml2/http-post/sso/",
+ "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/",
+ "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": ""
+ "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=="
}
-}
\ No newline at end of file
+}
diff --git a/demo-flask/index.py b/demo-flask/index.py
index 0f70b777..1f5404dc 100644
--- a/demo-flask/index.py
+++ b/demo-flask/index.py
@@ -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:
diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py
index d9b42a32..0b12bbe7 100644
--- a/src/onelogin/saml2/auth.py
+++ b/src/onelogin/saml2/auth.py
@@ -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
@@ -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()
@@ -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.
@@ -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.
@@ -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()
@@ -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
diff --git a/src/onelogin/saml2/logout_request.py b/src/onelogin/saml2/logout_request.py
index e419f585..1cb75efd 100644
--- a/src/onelogin/saml2/logout_request.py
+++ b/src/onelogin/saml2/logout_request.py
@@ -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.
@@ -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
@@ -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,
diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py
index 0ce801ea..cc5e8936 100644
--- a/src/onelogin/saml2/response.py
+++ b/src/onelogin/saml2/response.py
@@ -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
diff --git a/tests/src/OneLogin/saml2_tests/auth_test.py b/tests/src/OneLogin/saml2_tests/auth_test.py
index 070a4887..3a57c438 100644
--- a/tests/src/OneLogin/saml2_tests/auth_test.py
+++ b/tests/src/OneLogin/saml2_tests/auth_test.py
@@ -220,6 +220,9 @@ def testProcessResponseValid(self):
self.assertEqual(auth.get_attribute('mail'), attributes['mail'])
session_index = auth.get_session_index()
self.assertEqual('_6273d77b8cde0c333ec79d22a9fa0003b9fe2d75cb', session_index)
+ self.assertEqual("urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", auth.get_nameid_format())
+ self.assertIsNone(auth.get_nameid_nq())
+ self.assertEqual("http://stuff.com/endpoints/metadata.php", auth.get_nameid_spnq())
def testRedirectTo(self):
"""
@@ -1002,6 +1005,70 @@ def testGetNameIdFormat(self):
self.assertTrue(auth.is_authenticated())
self.assertEqual("urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified", auth.get_nameid_format())
+ def testGetNameIdNameQualifier(self):
+ """
+ Tests the get_nameid_nq method of the OneLogin_Saml2_Auth
+ """
+ settings = self.loadSettingsJSON()
+ message = self.file_contents(join(self.data_path, 'responses', 'valid_response_with_namequalifier.xml.base64'))
+ request_data = self.get_request()
+ request_data['post_data'] = {
+ 'SAMLResponse': message
+ }
+ auth = OneLogin_Saml2_Auth(request_data, old_settings=settings)
+ self.assertIsNone(auth.get_nameid_nq())
+ auth.process_response()
+ self.assertTrue(auth.is_authenticated())
+ self.assertEqual("https://test.example.com/saml/metadata", auth.get_nameid_nq())
+
+ def testGetNameIdNameQualifier2(self):
+ """
+ Tests the get_nameid_nq method of the OneLogin_Saml2_Auth
+ """
+ settings = self.loadSettingsJSON()
+ message = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64'))
+ request_data = self.get_request()
+ request_data['post_data'] = {
+ 'SAMLResponse': message
+ }
+ auth = OneLogin_Saml2_Auth(request_data, old_settings=settings)
+ self.assertIsNone(auth.get_nameid_nq())
+ auth.process_response()
+ self.assertTrue(auth.is_authenticated())
+ self.assertIsNone(auth.get_nameid_nq())
+
+ def testGetNameIdSPNameQualifier(self):
+ """
+ Tests the get_nameid_spnq method of the OneLogin_Saml2_Auth
+ """
+ settings = self.loadSettingsJSON()
+ message = self.file_contents(join(self.data_path, 'responses', 'valid_response_with_namequalifier.xml.base64'))
+ request_data = self.get_request()
+ request_data['post_data'] = {
+ 'SAMLResponse': message
+ }
+ auth = OneLogin_Saml2_Auth(request_data, old_settings=settings)
+ self.assertIsNone(auth.get_nameid_spnq())
+ auth.process_response()
+ self.assertTrue(auth.is_authenticated())
+ self.assertIsNone(auth.get_nameid_spnq())
+
+ def testGetNameIdSPNameQualifier2(self):
+ """
+ Tests the get_nameid_spnq method of the OneLogin_Saml2_Auth
+ """
+ settings = self.loadSettingsJSON()
+ message = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64'))
+ request_data = self.get_request()
+ request_data['post_data'] = {
+ 'SAMLResponse': message
+ }
+ auth = OneLogin_Saml2_Auth(request_data, old_settings=settings)
+ self.assertIsNone(auth.get_nameid_spnq())
+ auth.process_response()
+ self.assertTrue(auth.is_authenticated())
+ self.assertEqual("http://stuff.com/endpoints/metadata.php", auth.get_nameid_spnq())
+
def testBuildRequestSignature(self):
"""
Tests the build_request_signature method of the OneLogin_Saml2_Auth
diff --git a/tests/src/OneLogin/saml2_tests/logout_request_test.py b/tests/src/OneLogin/saml2_tests/logout_request_test.py
index eb50c785..6fd75d3a 100644
--- a/tests/src/OneLogin/saml2_tests/logout_request_test.py
+++ b/tests/src/OneLogin/saml2_tests/logout_request_test.py
@@ -193,7 +193,6 @@ def testGetNameIdData(self):
expected_name_id_data = {
'Format': 'urn:oasis:names:tc:SAML:2.0:nameid-format:emailAddress',
'NameQualifier': idp_data['entityId'],
- 'SPNameQualifier': sp_data['entityId'],
'Value': 'ONELOGIN_9c86c4542ab9d6fce07f2f7fd335287b9b3cdf69'
}
diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py
index bc172546..4794017b 100644
--- a/tests/src/OneLogin/saml2_tests/response_test.py
+++ b/tests/src/OneLogin/saml2_tests/response_test.py
@@ -285,6 +285,74 @@ def testReturnNameIdFormat(self):
with self.assertRaisesRegexp(Exception, 'An empty NameID value found'):
response_17.get_nameid_format()
+ def testReturnNameIdNameQualifier(self):
+ """
+ Tests the get_nameid_nq method of the OneLogin_Saml2_Response
+ """
+ json_settings = self.loadSettingsJSON()
+ json_settings['strict'] = False
+ settings = OneLogin_Saml2_Settings(json_settings)
+ xml = self.file_contents(join(self.data_path, 'responses', 'response1.xml.base64'))
+ response = OneLogin_Saml2_Response(settings, xml)
+ self.assertIsNone(response.get_nameid_nq())
+
+ xml_2 = self.file_contents(join(self.data_path, 'responses', 'response_encrypted_nameid.xml.base64'))
+ response_2 = OneLogin_Saml2_Response(settings, xml_2)
+ self.assertIsNone(response_2.get_nameid_nq())
+
+ xml_3 = self.file_contents(join(self.data_path, 'responses', 'valid_encrypted_assertion.xml.base64'))
+ response_3 = OneLogin_Saml2_Response(settings, xml_3)
+ self.assertIsNone(response_3.get_nameid_nq())
+
+ xml_4 = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64'))
+ response_4 = OneLogin_Saml2_Response(settings, xml_4)
+ self.assertIsNone(response_4.get_nameid_nq())
+
+ xml_5 = self.file_contents(join(self.data_path, 'responses', 'valid_response_with_namequalifier.xml.base64'))
+ response_5 = OneLogin_Saml2_Response(settings, xml_5)
+ self.assertEqual('https://test.example.com/saml/metadata', response_5.get_nameid_nq())
+
+ json_settings['strict'] = True
+ settings = OneLogin_Saml2_Settings(json_settings)
+ xml_6 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_nameid.xml.base64'))
+ response_6 = OneLogin_Saml2_Response(settings, xml_6)
+ with self.assertRaisesRegexp(Exception, 'NameID not found in the assertion of the Response'):
+ response_6.get_nameid_nq()
+
+ def testReturnNameIdNameSPQualifier(self):
+ """
+ Tests the get_nameid_spnq method of the OneLogin_Saml2_Response
+ """
+ json_settings = self.loadSettingsJSON()
+ json_settings['strict'] = False
+ settings = OneLogin_Saml2_Settings(json_settings)
+ xml = self.file_contents(join(self.data_path, 'responses', 'response1.xml.base64'))
+ response = OneLogin_Saml2_Response(settings, xml)
+ self.assertIsNone(response.get_nameid_spnq())
+
+ xml_2 = self.file_contents(join(self.data_path, 'responses', 'response_encrypted_nameid.xml.base64'))
+ response_2 = OneLogin_Saml2_Response(settings, xml_2)
+ self.assertEqual("http://stuff.com/endpoints/metadata.php", response_2.get_nameid_spnq())
+
+ xml_3 = self.file_contents(join(self.data_path, 'responses', 'valid_encrypted_assertion.xml.base64'))
+ response_3 = OneLogin_Saml2_Response(settings, xml_3)
+ self.assertEqual("http://stuff.com/endpoints/metadata.php", response_3.get_nameid_spnq())
+
+ xml_4 = self.file_contents(join(self.data_path, 'responses', 'valid_response.xml.base64'))
+ response_4 = OneLogin_Saml2_Response(settings, xml_4)
+ self.assertEqual("http://stuff.com/endpoints/metadata.php", response_4.get_nameid_spnq())
+
+ xml_5 = self.file_contents(join(self.data_path, 'responses', 'valid_response_with_namequalifier.xml.base64'))
+ response_5 = OneLogin_Saml2_Response(settings, xml_5)
+ self.assertIsNone(response_5.get_nameid_spnq())
+
+ json_settings['strict'] = True
+ settings = OneLogin_Saml2_Settings(json_settings)
+ xml_6 = self.file_contents(join(self.data_path, 'responses', 'invalids', 'no_nameid.xml.base64'))
+ response_6 = OneLogin_Saml2_Response(settings, xml_6)
+ with self.assertRaisesRegexp(Exception, 'NameID not found in the assertion of the Response'):
+ response_6.get_nameid_spnq()
+
def testGetNameIdData(self):
"""
Tests the get_nameid_data method of the OneLogin_Saml2_Response
From fd06de0eb966c01f791522317ea5e22fc4dddf1b Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Tue, 2 Jul 2019 20:19:27 +0200
Subject: [PATCH 174/255] Added get_in_response_to method to Response and
LogoutResponse classes
---
src/onelogin/saml2/logout_response.py | 21 ++++++++++++-------
src/onelogin/saml2/response.py | 10 ++++++++-
...lid_response_with_namequalifier.xml.base64 | 1 +
.../src/OneLogin/saml2_tests/response_test.py | 18 +++++++++++++++-
4 files changed, 41 insertions(+), 9 deletions(-)
create mode 100644 tests/data/responses/valid_response_with_namequalifier.xml.base64
diff --git a/src/onelogin/saml2/logout_response.py b/src/onelogin/saml2/logout_response.py
index 4af91a4f..6d326caf 100644
--- a/src/onelogin/saml2/logout_response.py
+++ b/src/onelogin/saml2/logout_response.py
@@ -101,14 +101,13 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False):
security = self.__settings.get_security_data()
+ in_response_to = self.get_in_response_to()
# Check if the InResponseTo of the Logout Response matches the ID of the Logout Request (requestId) if provided
- if request_id is not None and self.document.documentElement.hasAttribute('InResponseTo'):
- in_response_to = self.document.documentElement.getAttribute('InResponseTo')
- if request_id != in_response_to:
- raise OneLogin_Saml2_ValidationError(
- 'The InResponseTo of the Logout Response: %s, does not match the ID of the Logout request sent by the SP: %s' % (in_response_to, request_id),
- OneLogin_Saml2_ValidationError.WRONG_INRESPONSETO
- )
+ if request_id is not None and in_response_to and in_response_to != request_id:
+ raise OneLogin_Saml2_ValidationError(
+ 'The InResponseTo of the Logout Response: %s, does not match the ID of the Logout request sent by the SP: %s' % (in_response_to, request_id),
+ OneLogin_Saml2_ValidationError.WRONG_INRESPONSETO
+ )
# Check issuer
issuer = self.get_issuer()
@@ -237,6 +236,14 @@ def build(self, in_response_to):
self.__logout_response = logout_response
+ def get_in_response_to(self):
+ """
+ Gets the ID of the LogoutRequest which this response is in response to
+ :returns: ID of LogoutRequest this LogoutResponse is in response to or None if it is not present
+ :rtype: str
+ """
+ return self.document.documentElement.getAttribute('InResponseTo')
+
def get_response(self, deflate=True):
"""
Returns the Logout Response defated, base64encoded
diff --git a/src/onelogin/saml2/response.py b/src/onelogin/saml2/response.py
index cc5e8936..73c24f9a 100644
--- a/src/onelogin/saml2/response.py
+++ b/src/onelogin/saml2/response.py
@@ -133,7 +133,7 @@ def is_valid(self, request_data, request_id=None, raise_exceptions=False):
security = self.__settings.get_security_data()
current_url = OneLogin_Saml2_Utils.get_self_url_no_query(request_data)
- in_response_to = self.document.get('InResponseTo', None)
+ in_response_to = self.get_in_response_to()
if request_id is None and in_response_to is not None and security.get('rejectUnsolicitedResponsesWithInResponseTo', False):
raise OneLogin_Saml2_ValidationError(
'The Response has an InResponseTo attribute: %s while no InResponseTo was expected' % in_response_to,
@@ -405,6 +405,14 @@ def get_authn_contexts(self):
authn_context_nodes = self.__query_assertion('/saml:AuthnStatement/saml:AuthnContext/saml:AuthnContextClassRef')
return [OneLogin_Saml2_Utils.element_text(node) for node in authn_context_nodes]
+ def get_in_response_to(self):
+ """
+ Gets the ID of the request which this response is in response to
+ :returns: ID of AuthNRequest this Response is in response to or None if it is not present
+ :rtype: str
+ """
+ return self.document.get('InResponseTo')
+
def get_issuers(self):
"""
Gets the issuers (from message and from assertion)
diff --git a/tests/data/responses/valid_response_with_namequalifier.xml.base64 b/tests/data/responses/valid_response_with_namequalifier.xml.base64
new file mode 100644
index 00000000..a98de3ff
--- /dev/null
+++ b/tests/data/responses/valid_response_with_namequalifier.xml.base64
@@ -0,0 +1 @@
+PD94bWwgdmVyc2lvbj0iMS4wIj8+DQo8c2FtbHA6UmVzcG9uc2UgeG1sbnM6c2FtbHA9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpwcm90b2NvbCIgeG1sbnM6c2FtbD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgSUQ9InBmeDhmZWI5YWNkLTFlODYtYWMxMi05MDIzLTEzYjg0NDc5YjI1YiIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciPjxzYW1sOklzc3Vlcj5odHRwOi8vaWRwLmV4YW1wbGUuY29tLzwvc2FtbDpJc3N1ZXI+PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI+DQogIDxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+DQogICAgPGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNyc2Etc2hhMSIvPg0KICA8ZHM6UmVmZXJlbmNlIFVSST0iI3BmeDhmZWI5YWNkLTFlODYtYWMxMi05MDIzLTEzYjg0NDc5YjI1YiI+PGRzOlRyYW5zZm9ybXM+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI3NoYTEiLz48ZHM6RGlnZXN0VmFsdWU+NVRWZURYbGQ3YzhURmtybVlDeFpuL2ZHRTRzPTwvZHM6RGlnZXN0VmFsdWU+PC9kczpSZWZlcmVuY2U+PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT5hZlFaVUE2REpHa0hLNjVMMENBaTJBSDJkOWNwbExuekNPTHBCYm9hUmVmaWdtVC92L0tJZGcyYXpWRzY2Ykk1aFA1NTBNR0c2ZVVzaWJ1N2N3ZytFbG9tejVBalE3dzlGZG8waHdWWWhib3JaSkN2TUxLUzBEWkFzc01XZnZ3RGNUNmhra3UreXFlS2RhZ1BBOTYwQ25YcUMxeHpjMk43WS82dlBCU081bVU9PC9kczpTaWduYXR1cmVWYWx1ZT4NCjxkczpLZXlJbmZvPjxkczpYNTA5RGF0YT48ZHM6WDUwOUNlcnRpZmljYXRlPk1JSUNnVENDQWVvQ0NRQ2JPbHJXRGRYN0ZUQU5CZ2txaGtpRzl3MEJBUVVGQURDQmhERUxNQWtHQTFVRUJoTUNUazh4R0RBV0JnTlZCQWdURDBGdVpISmxZWE1nVTI5c1ltVnlaekVNTUFvR0ExVUVCeE1EUm05dk1SQXdEZ1lEVlFRS0V3ZFZUa2xPUlZSVU1SZ3dGZ1lEVlFRREV3OW1aV2xrWlM1bGNteGhibWN1Ym04eElUQWZCZ2txaGtpRzl3MEJDUUVXRW1GdVpISmxZWE5BZFc1cGJtVjBkQzV1YnpBZUZ3MHdOekEyTVRVeE1qQXhNelZhRncwd056QTRNVFF4TWpBeE16VmFNSUdFTVFzd0NRWURWUVFHRXdKT1R6RVlNQllHQTFVRUNCTVBRVzVrY21WaGN5QlRiMnhpWlhKbk1Rd3dDZ1lEVlFRSEV3TkdiMjh4RURBT0JnTlZCQW9UQjFWT1NVNUZWRlF4R0RBV0JnTlZCQU1URDJabGFXUmxMbVZ5YkdGdVp5NXViekVoTUI4R0NTcUdTSWIzRFFFSkFSWVNZVzVrY21WaGMwQjFibWx1WlhSMExtNXZNSUdmTUEwR0NTcUdTSWIzRFFFQkFRVUFBNEdOQURDQmlRS0JnUURpdmJoUjdQNTE2eC9TM0JxS3h1cFFlMExPTm9saXVwaUJPZXNDTzNTSGJEcmwzK3E5SWJmbmZtRTA0ck51TWNQc0l4QjE2MVRkRHBJZXNMQ243YzhhUEhJU0tPdFBsQWVUWlNuYjhRQXU3YVJqWnEzK1BiclA1dVczVGNmQ0dQdEtUeXRIT2dlL09sSmJvMDc4ZFZoWFExNGQxRUR3WEpXMXJSWHVVdDRDOFFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0R0JBQ0RWZnA4NkhPYnFZK2U4QlVvV1E5K1ZNUXgxQVNEb2hCandPc2cyV3lrVXFSWEYrZExmY1VIOWRXUjYzQ3RaSUtGRGJTdE5vbVBuUXo3bmJLK29ueWd3QnNwVkVibkh1VWloWnEzWlVkbXVtUXFDdzRVdnMvMVV2cTNvck9vL1dKVmhUeXZMZ0ZWSzJRYXJRNC82N09aZkhkN1IrUE9CWGhvcGhTTXYxWk9vPC9kczpYNTA5Q2VydGlmaWNhdGU+PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8+PC9kczpTaWduYXR1cmU+PHNhbWxwOlN0YXR1cz48c2FtbHA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8+PC9zYW1scDpTdGF0dXM+PHNhbWw6QXNzZXJ0aW9uIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgSUQ9InBmeDQxN2ZiOTc2LTk0NGEtNDNiZi05ZTUyLWZiOWM1OTYxNzYxZiIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTQtMDItMTlUMDE6Mzc6MDFaIj48c2FtbDpJc3N1ZXI+aHR0cDovL2lkcC5leGFtcGxlLmNvbS88L3NhbWw6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPg0KICA8ZHM6U2lnbmVkSW5mbz48ZHM6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPg0KICAgIDxkczpTaWduYXR1cmVNZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjcnNhLXNoYTEiLz4NCiAgPGRzOlJlZmVyZW5jZSBVUkk9IiNwZng0MTdmYjk3Ni05NDRhLTQzYmYtOWU1Mi1mYjljNTk2MTc2MWYiPjxkczpUcmFuc2Zvcm1zPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwLzA5L3htbGRzaWcjZW52ZWxvcGVkLXNpZ25hdHVyZSIvPjxkczpUcmFuc2Zvcm0gQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybXM+PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNzaGExIi8+PGRzOkRpZ2VzdFZhbHVlPmxSbTJ3UW13ZGhmZVZuMDFaS1Ewb05CN1JqQT08L2RzOkRpZ2VzdFZhbHVlPjwvZHM6UmVmZXJlbmNlPjwvZHM6U2lnbmVkSW5mbz48ZHM6U2lnbmF0dXJlVmFsdWU+aktwQUNmMWkxR0FMSWQ5Y0liQlFsTkJQMVhpZDhhYXFKOUxyTkFIZ1lpR2VIc0NscldVUkZJREprOGI0T3RmdHdXTGZKeXBXbXgwWm15M2hpTTJyVHBIbDBLMGVqSFNsOS9Ed0pabkNEQW1CS1lhZ0ZFR0xxWXYwaXI0Y2lYaForTkdXSDY1czhBRlVibjU2SytaS3lpMFkwMWc4TmVqaS92OTNlZFZ6ZTZnPTwvZHM6U2lnbmF0dXJlVmFsdWU+DQo8ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE+PGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlDZ1RDQ0Flb0NDUUNiT2xyV0RkWDdGVEFOQmdrcWhraUc5dzBCQVFVRkFEQ0JoREVMTUFrR0ExVUVCaE1DVGs4eEdEQVdCZ05WQkFnVEQwRnVaSEpsWVhNZ1UyOXNZbVZ5WnpFTU1Bb0dBMVVFQnhNRFJtOXZNUkF3RGdZRFZRUUtFd2RWVGtsT1JWUlVNUmd3RmdZRFZRUURFdzltWldsa1pTNWxjbXhoYm1jdWJtOHhJVEFmQmdrcWhraUc5dzBCQ1FFV0VtRnVaSEpsWVhOQWRXNXBibVYwZEM1dWJ6QWVGdzB3TnpBMk1UVXhNakF4TXpWYUZ3MHdOekE0TVRReE1qQXhNelZhTUlHRU1Rc3dDUVlEVlFRR0V3Sk9UekVZTUJZR0ExVUVDQk1QUVc1a2NtVmhjeUJUYjJ4aVpYSm5NUXd3Q2dZRFZRUUhFd05HYjI4eEVEQU9CZ05WQkFvVEIxVk9TVTVGVkZReEdEQVdCZ05WQkFNVEQyWmxhV1JsTG1WeWJHRnVaeTV1YnpFaE1COEdDU3FHU0liM0RRRUpBUllTWVc1a2NtVmhjMEIxYm1sdVpYUjBMbTV2TUlHZk1BMEdDU3FHU0liM0RRRUJBUVVBQTRHTkFEQ0JpUUtCZ1FEaXZiaFI3UDUxNngvUzNCcUt4dXBRZTBMT05vbGl1cGlCT2VzQ08zU0hiRHJsMytxOUliZm5mbUUwNHJOdU1jUHNJeEIxNjFUZERwSWVzTENuN2M4YVBISVNLT3RQbEFlVFpTbmI4UUF1N2FSalpxMytQYnJQNXVXM1RjZkNHUHRLVHl0SE9nZS9PbEpibzA3OGRWaFhRMTRkMUVEd1hKVzFyUlh1VXQ0QzhRSURBUUFCTUEwR0NTcUdTSWIzRFFFQkJRVUFBNEdCQUNEVmZwODZIT2JxWStlOEJVb1dROStWTVF4MUFTRG9oQmp3T3NnMld5a1VxUlhGK2RMZmNVSDlkV1I2M0N0WklLRkRiU3ROb21QblF6N25iSytvbnlnd0JzcFZFYm5IdVVpaFpxM1pVZG11bVFxQ3c0VXZzLzFVdnEzb3JPby9XSlZoVHl2TGdGVksyUWFyUTQvNjdPWmZIZDdSK1BPQlhob3BoU012MVpPbzwvZHM6WDUwOUNlcnRpZmljYXRlPjwvZHM6WDUwOURhdGE+PC9kczpLZXlJbmZvPjwvZHM6U2lnbmF0dXJlPjxzYW1sOlN1YmplY3Q+PHNhbWw6TmFtZUlEIE5hbWVRdWFsaWZpZXI9Imh0dHBzOi8vdGVzdC5leGFtcGxlLmNvbS9zYW1sL21ldGFkYXRhIiBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OmVtYWlsQWRkcmVzcyI+NDkyODgyNjE1YWNmMzFjODA5NmI2MjcyNDVkNzZhZTUzMDM2YzA5MDwvc2FtbDpOYW1lSUQ+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbiBNZXRob2Q9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDpjbTpiZWFyZXIiPjxzYW1sOlN1YmplY3RDb25maXJtYXRpb25EYXRhIE5vdE9uT3JBZnRlcj0iMjA1NC0wOC0yM1QwNjo1NzowMVoiIFJlY2lwaWVudD0iaHR0cHM6Ly9waXRidWxrLm5vLWlwLm9yZy9uZXdvbmVsb2dpbi9kZW1vMS9pbmRleC5waHA/YWNzIiBJblJlc3BvbnNlVG89Ik9ORUxPR0lOXzVmZTlkNmU0OTliMmYwOTEzMjA2YWFiM2Y3MTkxNzI5MDQ5YmI4MDciLz48L3NhbWw6U3ViamVjdENvbmZpcm1hdGlvbj48L3NhbWw6U3ViamVjdD48c2FtbDpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAxNC0wMi0xOVQwMTozNjozMVoiIE5vdE9uT3JBZnRlcj0iMjA1NC0wOC0yM1QwNjo1NzowMVoiPjxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PHNhbWw6QXVkaWVuY2U+aHR0cDovL3N0dWZmLmNvbS9lbmRwb2ludHMvbWV0YWRhdGEucGhwPC9zYW1sOkF1ZGllbmNlPjwvc2FtbDpBdWRpZW5jZVJlc3RyaWN0aW9uPjwvc2FtbDpDb25kaXRpb25zPjxzYW1sOkF1dGhuU3RhdGVtZW50IEF1dGhuSW5zdGFudD0iMjAxNC0wMi0xOVQwMTozNzowMVoiIFNlc3Npb25Ob3RPbk9yQWZ0ZXI9IjIwNTQtMDItMTlUMDk6Mzc6MDFaIiBTZXNzaW9uSW5kZXg9Il82MjczZDc3YjhjZGUwYzMzM2VjNzlkMjJhOWZhMDAwM2I5ZmUyZDc1Y2IiPjxzYW1sOkF1dGhuQ29udGV4dD48c2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZDwvc2FtbDpBdXRobkNvbnRleHRDbGFzc1JlZj48L3NhbWw6QXV0aG5Db250ZXh0Pjwvc2FtbDpBdXRoblN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PHNhbWw6QXR0cmlidXRlIE5hbWU9InVpZCIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+c21hcnRpbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJtYWlsIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5zbWFydGluQHlhY28uZXM8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgTmFtZT0iY24iIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhzaTp0eXBlPSJ4czpzdHJpbmciPlNpeHRvMzwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJzbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+TWFydGluMjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBOYW1lPSJlZHVQZXJzb25BZmZpbGlhdGlvbiIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeHNpOnR5cGU9InhzOnN0cmluZyI+dXNlcjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4c2k6dHlwZT0ieHM6c3RyaW5nIj5hZG1pbjwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PC9zYW1sOkFzc2VydGlvbj48L3NhbWxwOlJlc3BvbnNlPg==
\ No newline at end of file
diff --git a/tests/src/OneLogin/saml2_tests/response_test.py b/tests/src/OneLogin/saml2_tests/response_test.py
index 4794017b..aa180c05 100644
--- a/tests/src/OneLogin/saml2_tests/response_test.py
+++ b/tests/src/OneLogin/saml2_tests/response_test.py
@@ -467,7 +467,7 @@ def testGetNameIdData(self):
settings = OneLogin_Saml2_Settings(json_settings)
response_13 = OneLogin_Saml2_Response(settings, xml_6)
nameid_data_13 = response_13.get_nameid_data()
- nameid_data_13 = self.assertEqual(expected_nameid_data_5, nameid_data_13)
+ self.assertEqual(expected_nameid_data_5, nameid_data_13)
json_settings['strict'] = False
json_settings['security']['wantNameId'] = False
@@ -745,6 +745,22 @@ def testGetSessionNotOnOrAfter(self):
response_3 = OneLogin_Saml2_Response(settings, xml_3)
self.assertEqual(2696012228, response_3.get_session_not_on_or_after())
+ def testGetInResponseTo(self):
+ """
+ Tests the retrieval of the InResponseTo attribute
+ """
+
+ settings = OneLogin_Saml2_Settings(self.loadSettingsJSON())
+
+ # Response without an InResponseTo element should return None
+ xml = self.file_contents(join(self.data_path, 'responses', 'response1.xml.base64'))
+ response = OneLogin_Saml2_Response(settings, xml)
+ self.assertIsNone(response.get_in_response_to())
+
+ xml_3 = self.file_contents(join(self.data_path, 'responses', 'valid_encrypted_assertion.xml.base64'))
+ response_3 = OneLogin_Saml2_Response(settings, xml_3)
+ self.assertEqual('ONELOGIN_be60b8caf8e9d19b7a3551b244f116c947ff247d', response_3.get_in_response_to())
+
def testIsInvalidXML(self):
"""
Tests the is_valid method of the OneLogin_Saml2_Response
From 484f381a7266bdbd5e96371cffcd22a8f4cec180 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Tue, 2 Jul 2019 22:17:35 +0200
Subject: [PATCH 175/255] Release 2.6.0
---
changelog.md | 9 +++++++++
setup.py | 2 +-
2 files changed, 10 insertions(+), 1 deletion(-)
diff --git a/changelog.md b/changelog.md
index 91de981e..97fbc946 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,4 +1,13 @@
# python-saml changelog
+### 2.6.0 (Jul 02, 2019)
+* 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
+* Added get_in_response_to method to Response and LogoutResponse classes
+* Add get_last_authn_contexts method
+* Fix bug on friendlyName/nameFormat parameters on RequestedAttribute elements. Wrong variable name caused FriendlyName to overwrite NameFormat
+* Add support for Subjects on AuthNRequests by the new name_id_value_req parameeter.Fix testshib test. Improve README: Added inline markup to important references
+* Update defusedxml
+* Fix path in flask demo
+
### 2.5.0 (Jan 29, 2019)
* Security improvements. Use of tagid to prevent XPath injection. Disable DTD on fromstring defusedxml method
* [#239](https://github.com/onelogin/python-saml/issues/239) Check that the response has all of the AuthnContexts that we provided
diff --git a/setup.py b/setup.py
index 74f81d58..462bf96f 100644
--- a/setup.py
+++ b/setup.py
@@ -9,7 +9,7 @@
setup(
name='python-saml',
- version='2.5.0',
+ version='2.6.0',
description='Onelogin Python Toolkit. Add SAML support to your Python software using this library',
classifiers=[
'Development Status :: 5 - Production/Stable',
From e35150b87680289fd9e5b5b839d06f701d7f79bf Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Wed, 4 Sep 2019 00:06:09 +0200
Subject: [PATCH 176/255] Fix docstring of get_attribute method
---
src/onelogin/saml2/auth.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/onelogin/saml2/auth.py b/src/onelogin/saml2/auth.py
index 0b12bbe7..188e289e 100644
--- a/src/onelogin/saml2/auth.py
+++ b/src/onelogin/saml2/auth.py
@@ -315,7 +315,7 @@ def get_attribute(self, name):
:param name: Name of the attribute
:type name: string
- :returns: Attribute value if exists or []
+ :returns: Attribute value if exists or None
:rtype: string
"""
assert isinstance(name, basestring)
From a63ba08b83cd39d2a196dd01c229b95a90666649 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Wed, 11 Sep 2019 16:25:49 +0200
Subject: [PATCH 177/255] Fix CI
---
.travis.yml | 3 +--
demo-flask/index.py | 8 ++++----
tests/src/OneLogin/saml2_tests/logout_request_test.py | 1 -
3 files changed, 5 insertions(+), 7 deletions(-)
diff --git a/.travis.yml b/.travis.yml
index 7221c335..2d923162 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,7 +1,6 @@
language: python
python:
- - '2.7.6'
-# - '2.7.12'
+ - '2.7'
install:
- sudo apt-get update -qq
diff --git a/demo-flask/index.py b/demo-flask/index.py
index 1f5404dc..e4c11c6f 100644
--- a/demo-flask/index.py
+++ b/demo-flask/index.py
@@ -68,10 +68,10 @@ def index():
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)
+ # 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:
request_id = None
if 'AuthNRequestID' in session:
diff --git a/tests/src/OneLogin/saml2_tests/logout_request_test.py b/tests/src/OneLogin/saml2_tests/logout_request_test.py
index 6fd75d3a..de782f75 100644
--- a/tests/src/OneLogin/saml2_tests/logout_request_test.py
+++ b/tests/src/OneLogin/saml2_tests/logout_request_test.py
@@ -189,7 +189,6 @@ def testGetNameIdData(self):
OneLogin_Saml2_Logout_Request.get_nameid_data(dom_2.toxml(), key)
idp_data = settings.get_idp_data()
- sp_data = settings.get_sp_data()
expected_name_id_data = {
'Format': 'urn:oasis:names:tc:SAML:2.0:nameid-format:emailAddress',
'NameQualifier': idp_data['entityId'],
From 8bb4901efa89435466d0087f66efbb5c3f053f44 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Wed, 11 Sep 2019 16:38:49 +0200
Subject: [PATCH 178/255] Set true as the default value for strict setting
---
src/onelogin/saml2/settings.py | 4 ++--
tests/src/OneLogin/saml2_tests/settings_test.py | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/onelogin/saml2/settings.py b/src/onelogin/saml2/settings.py
index c210c5b3..75498ebe 100644
--- a/src/onelogin/saml2/settings.py
+++ b/src/onelogin/saml2/settings.py
@@ -72,7 +72,7 @@ def __init__(self, settings=None, custom_base_path=None, sp_validation_only=Fals
"""
self.__sp_validation_only = sp_validation_only
self.__paths = {}
- self.__strict = False
+ self.__strict = True
self.__debug = False
self.__sp = {}
self.__idp = {}
@@ -205,7 +205,7 @@ def __load_settings_from_dict(self, settings):
self.__sp = settings['sp']
self.__idp = settings.get('idp', {})
- self.__strict = settings.get('strict', False)
+ self.__strict = settings.get('strict', True)
self.__debug = settings.get('debug', False)
self.__security = settings.get('security', {})
self.__contacts = settings.get('contactPerson', {})
diff --git a/tests/src/OneLogin/saml2_tests/settings_test.py b/tests/src/OneLogin/saml2_tests/settings_test.py
index 8d02d041..bf2b547d 100644
--- a/tests/src/OneLogin/saml2_tests/settings_test.py
+++ b/tests/src/OneLogin/saml2_tests/settings_test.py
@@ -718,7 +718,7 @@ def testIsStrict(self):
del settings_info['strict']
settings = OneLogin_Saml2_Settings(settings_info)
- self.assertFalse(settings.is_strict())
+ self.assertTrue(settings.is_strict())
settings_info['strict'] = False
settings_2 = OneLogin_Saml2_Settings(settings_info)
From 55f1fcbded4faffc1796c1f79ee3bfb0a20f2635 Mon Sep 17 00:00:00 2001
From: Sixto Martin
Date: Wed, 11 Sep 2019 16:39:43 +0200
Subject: [PATCH 179/255] Release 2.7.0
---
README.md | 2 ++
changelog.md | 3 +++
setup.py | 2 +-
3 files changed, 6 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 5f6865a5..0f988a1c 100644
--- a/README.md
+++ b/README.md
@@ -14,6 +14,8 @@ Python3: [python3-saml](https://github.com/onelogin/python3-saml).
#### Warning ####
+Version 2.7.0 sets strict mode active by default
+
Update ``python-saml`` to ``2.5.0``, this version includes security improvements for preventing XEE and Xpath Injections.
Update ``python-saml`` to ``2.4.0``, this version includes a fix for the [CVE-2017-11427](https://www.cvedetails.com/cve/CVE-2017-11427/) vulnerability.
diff --git a/changelog.md b/changelog.md
index 97fbc946..8ce586f9 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,4 +1,7 @@
# python-saml changelog
+### 2.7.0 (Sep 11, 2019)
+* Set true as the default value for strict setting
+
### 2.6.0 (Jul 02, 2019)
* 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
* Added get_in_response_to method to Response and LogoutResponse classes
diff --git a/setup.py b/setup.py
index 462bf96f..7d545fb0 100644
--- a/setup.py
+++ b/setup.py
@@ -9,7 +9,7 @@
setup(
name='python-saml',
- version='2.6.0',
+ version='2.7.0',
description='Onelogin Python Toolkit. Add SAML support to your Python software using this library',
classifiers=[
'Development Status :: 5 - Production/Stable',
From 2ffc45b253a4c63a04127af31a7d103cf6ee53c4 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sat, 2 Nov 2019 09:43:00 +0000
Subject: [PATCH 180/255] Bump django from 1.11 to 1.11.23 in /demo-django
Bumps [django](https://github.com/django/django) from 1.11 to 1.11.23.
- [Release notes](https://github.com/django/django/releases)
- [Commits](https://github.com/django/django/compare/1.11...1.11.23)
Signed-off-by: dependabot[bot]
---
demo-django/requirements.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/demo-django/requirements.txt b/demo-django/requirements.txt
index f305b2f2..d0d5a268 100644
--- a/demo-django/requirements.txt
+++ b/demo-django/requirements.txt
@@ -1 +1 @@
-Django==1.11
+Django==1.11.23
From fb2a89a9fd3b068d6ea38988411db88bc1e73b6c Mon Sep 17 00:00:00 2001
From: Sergio Pulgarin
Date: Wed, 13 Nov 2019 16:17:15 -0500
Subject: [PATCH 181/255] Fixes minor typos with word "enough" (was "enought)
Addressing https://github.com/onelogin/python-saml/issues/261
---
docs/saml2/_modules/saml2/settings.html | 6 +++---
src/onelogin/saml2/settings.py | 4 ++--
tests/src/OneLogin/saml2_tests/settings_test.py | 2 +-
3 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/docs/saml2/_modules/saml2/settings.html b/docs/saml2/_modules/saml2/settings.html
index 42800b51..5f8f5c2c 100644
--- a/docs/saml2/_modules/saml2/settings.html
+++ b/docs/saml2/_modules/saml2/settings.html
@@ -406,7 +406,7 @@ Source code for saml2.settings
contact = settings [ 'contactPerson' ][ t ]
if (( 'givenName' not in contact or len ( contact [ 'givenName' ]) == 0 ) or
( 'emailAddress' not in contact or len ( contact [ 'emailAddress' ]) == 0 )):
- errors . append ( 'contact_not_enought_data' )
+ errors . append ( 'contact_not_enough_data' )
break
if 'organization' in settings :
@@ -415,7 +415,7 @@ Source code for saml2.settings
if (( 'name' not in organization or len ( organization [ 'name' ]) == 0 ) or
( 'displayname' not in organization or len ( organization [ 'displayname' ]) == 0 ) or
( 'url' not in organization or len ( organization [ 'url' ]) == 0 )):
- errors . append ( 'organization_not_enought_data' )
+ errors . append ( 'organization_not_enough_data' )
break
return errors
@@ -692,4 +692,4 @@ Navigation
Created using Sphinx 1.1.3.